You've already forked AstralRinth
forked from didirus/AstralRinth
Optimize user-generated images for reduced bandwidth (#961)
* Optimize user-generated images for reduced bandwidth * run prepare * Finish compression
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT o.id, o.slug, o.name, o.team_id, o.description, o.icon_url, o.color\n FROM organizations o\n WHERE o.id = ANY($1) OR LOWER(o.slug) = ANY($2)\n GROUP BY o.id;\n ",
|
||||
"query": "\n SELECT o.id, o.slug, o.name, o.team_id, o.description, o.icon_url, o.raw_icon_url, o.color\n FROM organizations o\n WHERE o.id = ANY($1) OR LOWER(o.slug) = ANY($2)\n GROUP BY o.id;\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -35,6 +35,11 @@
|
||||
},
|
||||
{
|
||||
"ordinal": 6,
|
||||
"name": "raw_icon_url",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 7,
|
||||
"name": "color",
|
||||
"type_info": "Int4"
|
||||
}
|
||||
@@ -52,8 +57,9 @@
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "28e5a9147061e78c0c1574ff650a30ead9fe7883d283e08a46155382e7a6c163"
|
||||
"hash": "06f51ba9bfc8ddf76c3ac2ad0a93849a3ff19649835bcdb1d44697d9a229725a"
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n UPDATE organizations\n SET icon_url = $1, color = $2\n WHERE (id = $3)\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Varchar",
|
||||
"Int4",
|
||||
"Int8"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "0eb293a353be47c61620922634cc339eda0e2422fcc602d7506c7cdf6152c928"
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n UPDATE users\n SET avatar_url = $1\n WHERE (id = $2)\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Varchar",
|
||||
"Int8"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "1ab781d26c93aa74bf90b78b74b99e50004d25d42d56b734e5e83f2333d0c0d2"
|
||||
}
|
||||
33
.sqlx/query-1d09169d25a30f4495778b0695ac00e48d682db15963a01195bbd5f981178ce9.json
generated
Normal file
33
.sqlx/query-1d09169d25a30f4495778b0695ac00e48d682db15963a01195bbd5f981178ce9.json
generated
Normal file
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n INSERT INTO users (\n id, username, email,\n avatar_url, raw_avatar_url, bio, created,\n github_id, discord_id, gitlab_id, google_id, steam_id, microsoft_id,\n email_verified, password, paypal_id, paypal_country, paypal_email,\n venmo_handle, stripe_customer_id\n )\n VALUES (\n $1, $2, $3, $4, $5,\n $6, $7,\n $8, $9, $10, $11, $12, $13,\n $14, $15, $16, $17, $18, $19, $20\n )\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8",
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Text",
|
||||
"Varchar",
|
||||
"Timestamptz",
|
||||
"Int8",
|
||||
"Int8",
|
||||
"Int8",
|
||||
"Varchar",
|
||||
"Int8",
|
||||
"Varchar",
|
||||
"Bool",
|
||||
"Text",
|
||||
"Text",
|
||||
"Text",
|
||||
"Text",
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "1d09169d25a30f4495778b0695ac00e48d682db15963a01195bbd5f981178ce9"
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT c.id id, c.name name, c.description description,\n c.icon_url icon_url, c.color color, c.created created, c.user_id user_id,\n c.updated updated, c.status status,\n ARRAY_AGG(DISTINCT cm.mod_id) filter (where cm.mod_id is not null) mods\n FROM collections c\n LEFT JOIN collections_mods cm ON cm.collection_id = c.id\n WHERE c.id = ANY($1)\n GROUP BY c.id;\n ",
|
||||
"query": "\n SELECT c.id id, c.name name, c.description description,\n c.icon_url icon_url, c.raw_icon_url raw_icon_url, c.color color, c.created created, c.user_id user_id,\n c.updated updated, c.status status,\n ARRAY_AGG(DISTINCT cm.mod_id) filter (where cm.mod_id is not null) mods\n FROM collections c\n LEFT JOIN collections_mods cm ON cm.collection_id = c.id\n WHERE c.id = ANY($1)\n GROUP BY c.id;\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -25,31 +25,36 @@
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "raw_icon_url",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"name": "color",
|
||||
"type_info": "Int4"
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"ordinal": 6,
|
||||
"name": "created",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 6,
|
||||
"ordinal": 7,
|
||||
"name": "user_id",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 7,
|
||||
"ordinal": 8,
|
||||
"name": "updated",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 8,
|
||||
"ordinal": 9,
|
||||
"name": "status",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 9,
|
||||
"ordinal": 10,
|
||||
"name": "mods",
|
||||
"type_info": "Int8Array"
|
||||
}
|
||||
@@ -65,6 +70,7 @@
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
@@ -72,5 +78,5 @@
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "4fc11e55884d6813992fba1d0b3111742a5f98453942fe83e09c2056bda401f4"
|
||||
"hash": "1d28c47c125cb4c6cff8ff373a56484d43a485f607a66b0753de07aceb02d274"
|
||||
}
|
||||
@@ -1,16 +1,17 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n UPDATE mods\n SET icon_url = $1, color = $2\n WHERE (id = $3)\n ",
|
||||
"query": "\n UPDATE mods\n SET icon_url = $1, raw_icon_url = $2, color = $3\n WHERE (id = $4)\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Varchar",
|
||||
"Text",
|
||||
"Int4",
|
||||
"Int8"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "73d77f11f97a9073f601119c6eb450ea08ae1d2df1a27ba9af1efa972ed9a836"
|
||||
"hash": "2647c3691a9809ebe28d1780d3efe922d2308b35838a4c0a7947cb1a0b32f3e1"
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT o.id, o.slug, o.name, o.team_id, o.description, o.icon_url, o.color\n FROM organizations o\n LEFT JOIN mods m ON m.organization_id = o.id\n WHERE m.id = $1\n GROUP BY o.id;\n ",
|
||||
"query": "\n SELECT o.id, o.slug, o.name, o.team_id, o.description, o.icon_url, o.raw_icon_url, o.color\n FROM organizations o\n LEFT JOIN mods m ON m.organization_id = o.id\n WHERE m.id = $1\n GROUP BY o.id;\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -35,6 +35,11 @@
|
||||
},
|
||||
{
|
||||
"ordinal": 6,
|
||||
"name": "raw_icon_url",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 7,
|
||||
"name": "color",
|
||||
"type_info": "Int4"
|
||||
}
|
||||
@@ -51,8 +56,9 @@
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "c0f23758b879b8a1304ad895b9bcf52b90913f23f96d16c24137b0d529a7475a"
|
||||
"hash": "31de3bbe7e8768bf32ae60d1cd32a26032e5cdb0e7f64eb4ac791855d7256cc8"
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n INSERT INTO users (\n id, username, email,\n avatar_url, bio, created,\n github_id, discord_id, gitlab_id, google_id, steam_id, microsoft_id,\n email_verified, password, paypal_id, paypal_country, paypal_email,\n venmo_handle, stripe_customer_id\n )\n VALUES (\n $1, $2, $3, $4, $5,\n $6, $7,\n $8, $9, $10, $11, $12, $13,\n $14, $15, $16, $17, $18, $19\n )\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8",
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Timestamptz",
|
||||
"Int8",
|
||||
"Int8",
|
||||
"Int8",
|
||||
"Varchar",
|
||||
"Int8",
|
||||
"Varchar",
|
||||
"Bool",
|
||||
"Text",
|
||||
"Text",
|
||||
"Text",
|
||||
"Text",
|
||||
"Text",
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "55f7a2e7fa37697bbcd7031b2bff742ae3921c1f2067723ff13f06328debee3e"
|
||||
}
|
||||
@@ -1,10 +1,11 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n UPDATE oauth_clients\n SET name = $1, icon_url = $2, max_scopes = $3, url = $4, description = $5\n WHERE (id = $6)\n ",
|
||||
"query": "\n UPDATE oauth_clients\n SET name = $1, icon_url = $2, raw_icon_url = $3, max_scopes = $4, url = $5, description = $6\n WHERE (id = $7)\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text",
|
||||
"Text",
|
||||
"Int8",
|
||||
@@ -15,5 +16,5 @@
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "781f54f36b8713bd36f5f89c98f8c89e94cc7c54e0b7302c877586186bb128a2"
|
||||
"hash": "5b6249b416a36ffc357aaf5060ebe960d5f54fe42f29f132de0e2925b5182f7f"
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT\n clients.id as \"id!\",\n clients.name as \"name!\",\n clients.icon_url as \"icon_url?\",\n clients.max_scopes as \"max_scopes!\",\n clients.secret_hash as \"secret_hash!\",\n clients.created as \"created!\",\n clients.created_by as \"created_by!\",\n clients.url as \"url?\",\n clients.description as \"description?\",\n uris.uri_ids as \"uri_ids?\",\n uris.uri_vals as \"uri_vals?\"\n FROM oauth_clients clients\n LEFT JOIN (\n SELECT client_id, array_agg(id) as uri_ids, array_agg(uri) as uri_vals\n FROM oauth_client_redirect_uris\n GROUP BY client_id\n ) uris ON clients.id = uris.client_id\n WHERE clients.id = ANY($1::bigint[])",
|
||||
"query": "\n SELECT\n clients.id as \"id!\",\n clients.name as \"name!\",\n clients.icon_url as \"icon_url?\",\n clients.raw_icon_url as \"raw_icon_url?\",\n clients.max_scopes as \"max_scopes!\",\n clients.secret_hash as \"secret_hash!\",\n clients.created as \"created!\",\n clients.created_by as \"created_by!\",\n clients.url as \"url?\",\n clients.description as \"description?\",\n uris.uri_ids as \"uri_ids?\",\n uris.uri_vals as \"uri_vals?\"\n FROM oauth_clients clients\n LEFT JOIN (\n SELECT client_id, array_agg(id) as uri_ids, array_agg(uri) as uri_vals\n FROM oauth_client_redirect_uris\n GROUP BY client_id\n ) uris ON clients.id = uris.client_id\n WHERE clients.id = ANY($1::bigint[])",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -20,41 +20,46 @@
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "raw_icon_url?",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "max_scopes!",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"ordinal": 5,
|
||||
"name": "secret_hash!",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"ordinal": 6,
|
||||
"name": "created!",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 6,
|
||||
"ordinal": 7,
|
||||
"name": "created_by!",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 7,
|
||||
"ordinal": 8,
|
||||
"name": "url?",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 8,
|
||||
"ordinal": 9,
|
||||
"name": "description?",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 9,
|
||||
"ordinal": 10,
|
||||
"name": "uri_ids?",
|
||||
"type_info": "Int8Array"
|
||||
},
|
||||
{
|
||||
"ordinal": 10,
|
||||
"ordinal": 11,
|
||||
"name": "uri_vals?",
|
||||
"type_info": "TextArray"
|
||||
}
|
||||
@@ -74,9 +79,10 @@
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
null,
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "93190af0fcfe2e795ed6b90eece7c567b8724509d1fdbd5cc75f516c1811c9d3"
|
||||
"hash": "5cb1aea414894c4720c7297a2bd8b411871a26b0163c4e87fba3b8988a0becff"
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n INSERT INTO organizations (id, slug, name, team_id, description, icon_url, color)\n VALUES ($1, $2, $3, $4, $5, $6, $7)\n ",
|
||||
"query": "\n INSERT INTO organizations (id, slug, name, team_id, description, icon_url, raw_icon_url, color)\n VALUES ($1, $2, $3, $4, $5, $6, $7, $8)\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
@@ -11,10 +11,11 @@
|
||||
"Int8",
|
||||
"Text",
|
||||
"Varchar",
|
||||
"Text",
|
||||
"Int4"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "a004ad357abfc01b4ab8a2e0a78e2b38f8a0edb7e3b2174d040ac4bb6e5bde39"
|
||||
"hash": "611107b00394bb57122d6a39b8d559abd50399d46f174a99ccfd9f76c3430892"
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n UPDATE organizations\n SET icon_url = NULL, color = NULL\n WHERE (id = $1)\n ",
|
||||
"query": "\n UPDATE organizations\n SET icon_url = NULL, raw_icon_url = NULL, color = NULL\n WHERE (id = $1)\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
@@ -10,5 +10,5 @@
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "91736b6bcc7a08c835cd3f3cea3a133ca42694df8fc3ce34b35d39bea6e1bba1"
|
||||
"hash": "6ae142b226a035ebdb35fe53930e298e24ab4d07e10881f24cdaa7f373b41797"
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT id, url, size, created, owner_id, context, mod_id, version_id, thread_message_id, report_id\n FROM uploaded_images\n WHERE id = ANY($1)\n GROUP BY id;\n ",
|
||||
"query": "\n SELECT id, url, raw_url, size, created, owner_id, context, mod_id, version_id, thread_message_id, report_id\n FROM uploaded_images\n WHERE id = ANY($1)\n GROUP BY id;\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -15,41 +15,46 @@
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "raw_url",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "size",
|
||||
"type_info": "Int4"
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"ordinal": 4,
|
||||
"name": "created",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"ordinal": 5,
|
||||
"name": "owner_id",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"ordinal": 6,
|
||||
"name": "context",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 6,
|
||||
"ordinal": 7,
|
||||
"name": "mod_id",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 7,
|
||||
"ordinal": 8,
|
||||
"name": "version_id",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 8,
|
||||
"ordinal": 9,
|
||||
"name": "thread_message_id",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 9,
|
||||
"ordinal": 10,
|
||||
"name": "report_id",
|
||||
"type_info": "Int8"
|
||||
}
|
||||
@@ -66,11 +71,12 @@
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "64fe01f3dd84c51966150e1278189c04da9e5fcd994ef5162afb1321b9d4b643"
|
||||
"hash": "6cc4e708db6ba1fa1fffdc5e0e40a11b5c512c25b113df984ab2a9557c5f5232"
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT id, url, size, created, owner_id, context, mod_id, version_id, thread_message_id, report_id\n FROM uploaded_images\n WHERE context = $1\n AND (mod_id = $2 OR ($2 IS NULL AND mod_id IS NULL))\n AND (version_id = $3 OR ($3 IS NULL AND version_id IS NULL))\n AND (thread_message_id = $4 OR ($4 IS NULL AND thread_message_id IS NULL))\n AND (report_id = $5 OR ($5 IS NULL AND report_id IS NULL))\n GROUP BY id\n ",
|
||||
"query": "\n SELECT id, url, raw_url, size, created, owner_id, context, mod_id, version_id, thread_message_id, report_id\n FROM uploaded_images\n WHERE context = $1\n AND (mod_id = $2 OR ($2 IS NULL AND mod_id IS NULL))\n AND (version_id = $3 OR ($3 IS NULL AND version_id IS NULL))\n AND (thread_message_id = $4 OR ($4 IS NULL AND thread_message_id IS NULL))\n AND (report_id = $5 OR ($5 IS NULL AND report_id IS NULL))\n GROUP BY id\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -15,41 +15,46 @@
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "raw_url",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "size",
|
||||
"type_info": "Int4"
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"ordinal": 4,
|
||||
"name": "created",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"ordinal": 5,
|
||||
"name": "owner_id",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"ordinal": 6,
|
||||
"name": "context",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 6,
|
||||
"ordinal": 7,
|
||||
"name": "mod_id",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 7,
|
||||
"ordinal": 8,
|
||||
"name": "version_id",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 8,
|
||||
"ordinal": 9,
|
||||
"name": "thread_message_id",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 9,
|
||||
"ordinal": 10,
|
||||
"name": "report_id",
|
||||
"type_info": "Int8"
|
||||
}
|
||||
@@ -70,11 +75,12 @@
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "1d6a53187082ad9a57294d9f1c13d66131ccc3d4a0cf59d42346474196ea50f8"
|
||||
"hash": "74b354b3b79eba18040f8dcf401dd872a08f497d6628147a81b3e015e3a35ad8"
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n INSERT INTO oauth_clients (\n id, name, icon_url, max_scopes, secret_hash, created_by\n )\n VALUES (\n $1, $2, $3, $4, $5, $6\n )\n ",
|
||||
"query": "\n INSERT INTO oauth_clients (\n id, name, icon_url, raw_icon_url, max_scopes, secret_hash, created_by\n )\n VALUES (\n $1, $2, $3, $4, $5, $6, $7\n )\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
@@ -8,6 +8,7 @@
|
||||
"Int8",
|
||||
"Text",
|
||||
"Text",
|
||||
"Text",
|
||||
"Int8",
|
||||
"Text",
|
||||
"Int8"
|
||||
@@ -15,5 +16,5 @@
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "93f6a94a9b288916dbf9999338d2278605311a311def3cbe38846b8ca465737f"
|
||||
"hash": "7c4b975bea12f16d66ed5f663fcd40097d9a09aad8bec6eec9639d56a197aeca"
|
||||
}
|
||||
34
.sqlx/query-87a0f2f991d749876d90b8e4ab73727f638a019b64e6cb1d891b333c2f09099c.json
generated
Normal file
34
.sqlx/query-87a0f2f991d749876d90b8e4ab73727f638a019b64e6cb1d891b333c2f09099c.json
generated
Normal file
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT id, image_url, raw_image_url FROM mods_gallery\n WHERE image_url = $1\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "id",
|
||||
"type_info": "Int4"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "image_url",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "raw_image_url",
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "87a0f2f991d749876d90b8e4ab73727f638a019b64e6cb1d891b333c2f09099c"
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n INSERT INTO collections (\n id, user_id, name, description, \n created, icon_url, status\n )\n VALUES (\n $1, $2, $3, $4, \n $5, $6, $7\n )\n ",
|
||||
"query": "\n INSERT INTO collections (\n id, user_id, name, description, \n created, icon_url, raw_icon_url, status\n )\n VALUES (\n $1, $2, $3, $4, \n $5, $6, $7, $8\n )\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
@@ -11,10 +11,11 @@
|
||||
"Varchar",
|
||||
"Timestamptz",
|
||||
"Varchar",
|
||||
"Text",
|
||||
"Varchar"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "5ba9860050d19de8fe81482cbbdc68b32092609cc7150c7fcf491f342c5d9770"
|
||||
"hash": "8d7746fedec4c2339352a3acd934b13c351b8a1fdb05bf982bab1a5b7ed17f3b"
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n UPDATE collections\n SET icon_url = $1, color = $2\n WHERE (id = $3)\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Varchar",
|
||||
"Int4",
|
||||
"Int8"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "9544cea57095a94109be5fef9a4737626a9003d58680943cdbffc7c9ada7877b"
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n UPDATE collections\n SET icon_url = NULL, color = NULL\n WHERE (id = $1)\n ",
|
||||
"query": "\n UPDATE collections\n SET icon_url = NULL, raw_icon_url = NULL, color = NULL\n WHERE (id = $1)\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
@@ -10,5 +10,5 @@
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "1abc74fe1da85e031edbc896797991337b57d2c47a8a978f9b9f34b20bf8f410"
|
||||
"hash": "aebefe598486b0e1a5c74eeb9e69f6322fd745fce730843337eb82d7ceff3a2f"
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n UPDATE mods\n SET icon_url = NULL, color = NULL\n WHERE (id = $1)\n ",
|
||||
"query": "\n UPDATE mods\n SET icon_url = NULL, raw_icon_url = NULL, color = NULL\n WHERE (id = $1)\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
@@ -10,5 +10,5 @@
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "04e5ecb14c526000e9098efb65861f6125e6fcc88f39d6ad811ac8504d229de1"
|
||||
"hash": "afdee0d5dce6cb037d3349aed3dc96fdc68092fca1a752c5df5c465406e2b4b5"
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT id, email,\n avatar_url, username, bio,\n created, role, badges,\n balance,\n github_id, discord_id, gitlab_id, google_id, steam_id, microsoft_id,\n email_verified, password, totp_secret, paypal_id, paypal_country, paypal_email,\n venmo_handle, stripe_customer_id\n FROM users\n WHERE id = ANY($1) OR LOWER(username) = ANY($2)\n ",
|
||||
"query": "\n SELECT id, email,\n avatar_url, raw_avatar_url, username, bio,\n created, role, badges,\n balance,\n github_id, discord_id, gitlab_id, google_id, steam_id, microsoft_id,\n email_verified, password, totp_secret, paypal_id, paypal_country, paypal_email,\n venmo_handle, stripe_customer_id\n FROM users\n WHERE id = ANY($1) OR LOWER(username) = ANY($2)\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -20,101 +20,106 @@
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "raw_avatar_url",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "username",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"ordinal": 5,
|
||||
"name": "bio",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"ordinal": 6,
|
||||
"name": "created",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 6,
|
||||
"ordinal": 7,
|
||||
"name": "role",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 7,
|
||||
"ordinal": 8,
|
||||
"name": "badges",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 8,
|
||||
"ordinal": 9,
|
||||
"name": "balance",
|
||||
"type_info": "Numeric"
|
||||
},
|
||||
{
|
||||
"ordinal": 9,
|
||||
"ordinal": 10,
|
||||
"name": "github_id",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 10,
|
||||
"ordinal": 11,
|
||||
"name": "discord_id",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 11,
|
||||
"ordinal": 12,
|
||||
"name": "gitlab_id",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 12,
|
||||
"ordinal": 13,
|
||||
"name": "google_id",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 13,
|
||||
"ordinal": 14,
|
||||
"name": "steam_id",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 14,
|
||||
"ordinal": 15,
|
||||
"name": "microsoft_id",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 15,
|
||||
"ordinal": 16,
|
||||
"name": "email_verified",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 16,
|
||||
"ordinal": 17,
|
||||
"name": "password",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 17,
|
||||
"ordinal": 18,
|
||||
"name": "totp_secret",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 18,
|
||||
"ordinal": 19,
|
||||
"name": "paypal_id",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 19,
|
||||
"ordinal": 20,
|
||||
"name": "paypal_country",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 20,
|
||||
"ordinal": 21,
|
||||
"name": "paypal_email",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 21,
|
||||
"ordinal": 22,
|
||||
"name": "venmo_handle",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 22,
|
||||
"ordinal": 23,
|
||||
"name": "stripe_customer_id",
|
||||
"type_info": "Text"
|
||||
}
|
||||
@@ -129,6 +134,7 @@
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
@@ -151,5 +157,5 @@
|
||||
true
|
||||
]
|
||||
},
|
||||
"hash": "93b8fff7ebe72e55e34e98bd365fe1866ec395146d5dabe071a13baa55ec3c09"
|
||||
"hash": "c4e7adb61382e0422439120f9a6a4388ab4ec25c0d81c2d5809cf011e49d0a6c"
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT m.id id, m.name name, m.summary summary, m.downloads downloads, m.follows follows,\n m.icon_url icon_url, m.description description, m.published published,\n m.updated updated, m.approved approved, m.queued, m.status status, m.requested_status requested_status,\n m.license_url license_url,\n m.team_id team_id, m.organization_id organization_id, m.license license, m.slug slug, m.moderation_message moderation_message, m.moderation_message_body moderation_message_body,\n m.webhook_sent, m.color,\n t.id thread_id, m.monetization_status monetization_status,\n ARRAY_AGG(DISTINCT c.category) filter (where c.category is not null and mc.is_additional is false) categories,\n ARRAY_AGG(DISTINCT c.category) filter (where c.category is not null and mc.is_additional is true) additional_categories\n FROM mods m\n INNER JOIN threads t ON t.mod_id = m.id\n LEFT JOIN mods_categories mc ON mc.joining_mod_id = m.id\n LEFT JOIN categories c ON mc.joining_category_id = c.id\n WHERE m.id = ANY($1) OR m.slug = ANY($2)\n GROUP BY t.id, m.id;\n ",
|
||||
"query": "\n SELECT m.id id, m.name name, m.summary summary, m.downloads downloads, m.follows follows,\n m.icon_url icon_url, m.raw_icon_url raw_icon_url, m.description description, m.published published,\n m.updated updated, m.approved approved, m.queued, m.status status, m.requested_status requested_status,\n m.license_url license_url,\n m.team_id team_id, m.organization_id organization_id, m.license license, m.slug slug, m.moderation_message moderation_message, m.moderation_message_body moderation_message_body,\n m.webhook_sent, m.color,\n t.id thread_id, m.monetization_status monetization_status,\n ARRAY_AGG(DISTINCT c.category) filter (where c.category is not null and mc.is_additional is false) categories,\n ARRAY_AGG(DISTINCT c.category) filter (where c.category is not null and mc.is_additional is true) additional_categories\n FROM mods m\n INNER JOIN threads t ON t.mod_id = m.id\n LEFT JOIN mods_categories mc ON mc.joining_mod_id = m.id\n LEFT JOIN categories c ON mc.joining_category_id = c.id\n WHERE m.id = ANY($1) OR m.slug = ANY($2)\n GROUP BY t.id, m.id;\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -35,101 +35,106 @@
|
||||
},
|
||||
{
|
||||
"ordinal": 6,
|
||||
"name": "raw_icon_url",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 7,
|
||||
"name": "description",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 7,
|
||||
"ordinal": 8,
|
||||
"name": "published",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 8,
|
||||
"ordinal": 9,
|
||||
"name": "updated",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 9,
|
||||
"ordinal": 10,
|
||||
"name": "approved",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 10,
|
||||
"ordinal": 11,
|
||||
"name": "queued",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 11,
|
||||
"ordinal": 12,
|
||||
"name": "status",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 12,
|
||||
"ordinal": 13,
|
||||
"name": "requested_status",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 13,
|
||||
"ordinal": 14,
|
||||
"name": "license_url",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 14,
|
||||
"ordinal": 15,
|
||||
"name": "team_id",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 15,
|
||||
"ordinal": 16,
|
||||
"name": "organization_id",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 16,
|
||||
"ordinal": 17,
|
||||
"name": "license",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 17,
|
||||
"ordinal": 18,
|
||||
"name": "slug",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 18,
|
||||
"ordinal": 19,
|
||||
"name": "moderation_message",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 19,
|
||||
"ordinal": 20,
|
||||
"name": "moderation_message_body",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 20,
|
||||
"ordinal": 21,
|
||||
"name": "webhook_sent",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 21,
|
||||
"ordinal": 22,
|
||||
"name": "color",
|
||||
"type_info": "Int4"
|
||||
},
|
||||
{
|
||||
"ordinal": 22,
|
||||
"ordinal": 23,
|
||||
"name": "thread_id",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 23,
|
||||
"ordinal": 24,
|
||||
"name": "monetization_status",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 24,
|
||||
"ordinal": 25,
|
||||
"name": "categories",
|
||||
"type_info": "VarcharArray"
|
||||
},
|
||||
{
|
||||
"ordinal": 25,
|
||||
"ordinal": 26,
|
||||
"name": "additional_categories",
|
||||
"type_info": "VarcharArray"
|
||||
}
|
||||
@@ -147,6 +152,7 @@
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
@@ -169,5 +175,5 @@
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "92b9298c0b6255b4121bf3079e121da06e6e0cdaa131cc9897cb321eaeb3d10b"
|
||||
"hash": "c7c400d74c478b4194f478f6d732a92c0b9a72e1cb6147009018b2712398c24f"
|
||||
}
|
||||
17
.sqlx/query-c91110c893f6d4cba9cb1cb149bca13ae8848d8eb02494fedee8fe4801529738.json
generated
Normal file
17
.sqlx/query-c91110c893f6d4cba9cb1cb149bca13ae8848d8eb02494fedee8fe4801529738.json
generated
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n UPDATE collections\n SET icon_url = $1, raw_icon_url = $2, color = $3\n WHERE (id = $4)\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Varchar",
|
||||
"Text",
|
||||
"Int4",
|
||||
"Int8"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "c91110c893f6d4cba9cb1cb149bca13ae8848d8eb02494fedee8fe4801529738"
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n INSERT INTO mods_gallery (\n mod_id, image_url, featured, name, description, ordering\n )\n SELECT * FROM UNNEST ($1::bigint[], $2::varchar[], $3::bool[], $4::varchar[], $5::varchar[], $6::bigint[])\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8Array",
|
||||
"VarcharArray",
|
||||
"BoolArray",
|
||||
"VarcharArray",
|
||||
"VarcharArray",
|
||||
"Int8Array"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "d5b2cfec04f4a74b5a3767047732e67c3107dcf4a386a4af552191460216f45d"
|
||||
}
|
||||
@@ -1,12 +1,13 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n INSERT INTO uploaded_images (\n id, url, size, created, owner_id, context, mod_id, version_id, thread_message_id, report_id\n )\n VALUES (\n $1, $2, $3, $4, $5, $6, $7, $8, $9, $10\n );\n ",
|
||||
"query": "\n INSERT INTO uploaded_images (\n id, url, raw_url, size, created, owner_id, context, mod_id, version_id, thread_message_id, report_id\n )\n VALUES (\n $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11\n );\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8",
|
||||
"Varchar",
|
||||
"Text",
|
||||
"Int4",
|
||||
"Timestamptz",
|
||||
"Int8",
|
||||
@@ -19,5 +20,5 @@
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "15fac93c76e72348b50f526e1acb183521d94be335ad8b9dfeb0398d4a8a2fc4"
|
||||
"hash": "dd2d94d97e4cc991041d871acd695a80ac298d9a6b2ea29e0b5be8d1bb10609c"
|
||||
}
|
||||
16
.sqlx/query-e1f69ecbeece313444909552d1e857a9e7795981b6a7b0d2ce92d6c59d61c9c9.json
generated
Normal file
16
.sqlx/query-e1f69ecbeece313444909552d1e857a9e7795981b6a7b0d2ce92d6c59d61c9c9.json
generated
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n UPDATE users\n SET avatar_url = $1, raw_avatar_url = $2\n WHERE (id = $3)\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Varchar",
|
||||
"Text",
|
||||
"Int8"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "e1f69ecbeece313444909552d1e857a9e7795981b6a7b0d2ce92d6c59d61c9c9"
|
||||
}
|
||||
20
.sqlx/query-e451e8ed4fb360f0f1bfe540a8e72a923dc0c4c1737387d97bb71076a29ea0f9.json
generated
Normal file
20
.sqlx/query-e451e8ed4fb360f0f1bfe540a8e72a923dc0c4c1737387d97bb71076a29ea0f9.json
generated
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n INSERT INTO mods_gallery (\n mod_id, image_url, raw_image_url, featured, name, description, ordering\n )\n SELECT * FROM UNNEST ($1::bigint[], $2::varchar[], $3::varchar[], $4::bool[], $5::varchar[], $6::varchar[], $7::bigint[])\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Int8Array",
|
||||
"VarcharArray",
|
||||
"VarcharArray",
|
||||
"BoolArray",
|
||||
"VarcharArray",
|
||||
"VarcharArray",
|
||||
"Int8Array"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "e451e8ed4fb360f0f1bfe540a8e72a923dc0c4c1737387d97bb71076a29ea0f9"
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT\n clients.id as \"id!\",\n clients.name as \"name!\",\n clients.icon_url as \"icon_url?\",\n clients.max_scopes as \"max_scopes!\",\n clients.secret_hash as \"secret_hash!\",\n clients.created as \"created!\",\n clients.created_by as \"created_by!\",\n clients.url as \"url?\",\n clients.description as \"description?\",\n uris.uri_ids as \"uri_ids?\",\n uris.uri_vals as \"uri_vals?\"\n FROM oauth_clients clients\n LEFT JOIN (\n SELECT client_id, array_agg(id) as uri_ids, array_agg(uri) as uri_vals\n FROM oauth_client_redirect_uris\n GROUP BY client_id\n ) uris ON clients.id = uris.client_id\n WHERE created_by = $1",
|
||||
"query": "\n SELECT\n clients.id as \"id!\",\n clients.name as \"name!\",\n clients.icon_url as \"icon_url?\",\n clients.raw_icon_url as \"raw_icon_url?\",\n clients.max_scopes as \"max_scopes!\",\n clients.secret_hash as \"secret_hash!\",\n clients.created as \"created!\",\n clients.created_by as \"created_by!\",\n clients.url as \"url?\",\n clients.description as \"description?\",\n uris.uri_ids as \"uri_ids?\",\n uris.uri_vals as \"uri_vals?\"\n FROM oauth_clients clients\n LEFT JOIN (\n SELECT client_id, array_agg(id) as uri_ids, array_agg(uri) as uri_vals\n FROM oauth_client_redirect_uris\n GROUP BY client_id\n ) uris ON clients.id = uris.client_id\n WHERE created_by = $1",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -20,41 +20,46 @@
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "raw_icon_url?",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "max_scopes!",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"ordinal": 5,
|
||||
"name": "secret_hash!",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"ordinal": 6,
|
||||
"name": "created!",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 6,
|
||||
"ordinal": 7,
|
||||
"name": "created_by!",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 7,
|
||||
"ordinal": 8,
|
||||
"name": "url?",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 8,
|
||||
"ordinal": 9,
|
||||
"name": "description?",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 9,
|
||||
"ordinal": 10,
|
||||
"name": "uri_ids?",
|
||||
"type_info": "Int8Array"
|
||||
},
|
||||
{
|
||||
"ordinal": 10,
|
||||
"ordinal": 11,
|
||||
"name": "uri_vals?",
|
||||
"type_info": "TextArray"
|
||||
}
|
||||
@@ -68,6 +73,7 @@
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
@@ -78,5 +84,5 @@
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "467df7989a1f4f5f3bc3409a4060e89dcfdd5a5ab9c7a96dc4008e74e06be914"
|
||||
"hash": "ebb2f650d476af928d57d4c3f2601c0295aca54829d28c223895a063e1672770"
|
||||
}
|
||||
17
.sqlx/query-f25ad90c1ac8c0c2fd0e8ba8fa7134335b6244b0685eba15aef5c8c02cc70d70.json
generated
Normal file
17
.sqlx/query-f25ad90c1ac8c0c2fd0e8ba8fa7134335b6244b0685eba15aef5c8c02cc70d70.json
generated
Normal file
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n UPDATE organizations\n SET icon_url = $1, raw_icon_url = $2, color = $3\n WHERE (id = $4)\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Varchar",
|
||||
"Text",
|
||||
"Int4",
|
||||
"Int8"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "f25ad90c1ac8c0c2fd0e8ba8fa7134335b6244b0685eba15aef5c8c02cc70d70"
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n INSERT INTO mods (\n id, team_id, name, summary, description,\n published, downloads, icon_url, status, requested_status,\n license_url, license,\n slug, color, monetization_status, organization_id\n )\n VALUES (\n $1, $2, $3, $4, $5, $6, \n $7, $8, $9, $10, \n $11, $12, \n LOWER($13), $14, $15, $16\n )\n ",
|
||||
"query": "\n INSERT INTO mods (\n id, team_id, name, summary, description,\n published, downloads, icon_url, raw_icon_url, status, requested_status,\n license_url, license,\n slug, color, monetization_status, organization_id\n )\n VALUES (\n $1, $2, $3, $4, $5, $6, \n $7, $8, $9, $10, $11,\n $12, $13,\n LOWER($14), $15, $16, $17\n )\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
@@ -13,6 +13,7 @@
|
||||
"Timestamptz",
|
||||
"Int4",
|
||||
"Varchar",
|
||||
"Text",
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
"Varchar",
|
||||
@@ -25,5 +26,5 @@
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "177b15719778b7788b88877af6affb8dba11da318b14dab7fcc7165c46bbecf5"
|
||||
"hash": "f899b378fad8fcfa1ebf527146b565b7c4466205e0bfd84f299123329926fe3f"
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n SELECT DISTINCT mod_id, mg.image_url, mg.featured, mg.name, mg.description, mg.created, mg.ordering\n FROM mods_gallery mg\n INNER JOIN mods m ON mg.mod_id = m.id\n WHERE m.id = ANY($1) OR m.slug = ANY($2)\n ",
|
||||
"query": "\n SELECT DISTINCT mod_id, mg.image_url, mg.raw_image_url, mg.featured, mg.name, mg.description, mg.created, mg.ordering\n FROM mods_gallery mg\n INNER JOIN mods m ON mg.mod_id = m.id\n WHERE m.id = ANY($1) OR m.slug = ANY($2)\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
@@ -15,26 +15,31 @@
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "raw_image_url",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "featured",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"ordinal": 4,
|
||||
"name": "name",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"ordinal": 5,
|
||||
"name": "description",
|
||||
"type_info": "Varchar"
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"ordinal": 6,
|
||||
"name": "created",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 6,
|
||||
"ordinal": 7,
|
||||
"name": "ordering",
|
||||
"type_info": "Int8"
|
||||
}
|
||||
@@ -46,6 +51,7 @@
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
@@ -55,5 +61,5 @@
|
||||
false
|
||||
]
|
||||
},
|
||||
"hash": "f62ec19e7e23ec98ad38f79ba28066f1b13a607923003699378bda895aab3a84"
|
||||
"hash": "ff7ff5b202609f1e68dc4a18eed1183b1077b15577b52083c2cf0b2cc0818a29"
|
||||
}
|
||||
46
Cargo.lock
generated
46
Cargo.lock
generated
@@ -734,6 +734,12 @@ version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
|
||||
|
||||
[[package]]
|
||||
name = "byteorder-lite"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495"
|
||||
|
||||
[[package]]
|
||||
name = "bytes"
|
||||
version = "1.6.0"
|
||||
@@ -1800,6 +1806,12 @@ version = "0.29.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd"
|
||||
|
||||
[[package]]
|
||||
name = "glob"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b"
|
||||
|
||||
[[package]]
|
||||
name = "governor"
|
||||
version = "0.6.3"
|
||||
@@ -2243,6 +2255,17 @@ dependencies = [
|
||||
"tiff",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "image"
|
||||
version = "0.25.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "99314c8a2152b8ddb211f924cdae532d8c5e4c8bb54728e12fff1b0cd5963a10"
|
||||
dependencies = [
|
||||
"bytemuck",
|
||||
"byteorder-lite",
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "indexmap"
|
||||
version = "1.9.3"
|
||||
@@ -2493,7 +2516,7 @@ dependencies = [
|
||||
"hmac 0.11.0",
|
||||
"hyper 0.14.29",
|
||||
"hyper-tls 0.5.0",
|
||||
"image",
|
||||
"image 0.24.9",
|
||||
"itertools 0.12.1",
|
||||
"jemallocator",
|
||||
"json-patch",
|
||||
@@ -2530,6 +2553,7 @@ dependencies = [
|
||||
"urlencoding",
|
||||
"uuid 1.9.1",
|
||||
"validator",
|
||||
"webp",
|
||||
"woothee",
|
||||
"xml-rs",
|
||||
"yaserde",
|
||||
@@ -2627,6 +2651,16 @@ dependencies = [
|
||||
"vcpkg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libwebp-sys"
|
||||
version = "0.9.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "54cd30df7c7165ce74a456e4ca9732c603e8dc5e60784558c1c6dc047f876733"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"glob",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libz-sys"
|
||||
version = "1.1.18"
|
||||
@@ -5631,6 +5665,16 @@ dependencies = [
|
||||
"wasm-bindgen",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "webp"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f53152f51fb5af0c08484c33d16cca96175881d1f3dec068c23b31a158c2d99"
|
||||
dependencies = [
|
||||
"image 0.25.2",
|
||||
"libwebp-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "webpki-roots"
|
||||
version = "0.25.4"
|
||||
|
||||
@@ -102,6 +102,7 @@ sentry-actix = "0.32.1"
|
||||
|
||||
image = "0.24.6"
|
||||
color-thief = "0.2.2"
|
||||
webp = "0.3.0"
|
||||
|
||||
woothee = "0.13.0"
|
||||
|
||||
|
||||
22
migrations/20240907192840_raw-images.sql
Normal file
22
migrations/20240907192840_raw-images.sql
Normal file
@@ -0,0 +1,22 @@
|
||||
ALTER TABLE mods ADD COLUMN raw_icon_url TEXT NULL;
|
||||
UPDATE mods SET raw_icon_url = icon_url;
|
||||
|
||||
ALTER TABLE users ADD COLUMN raw_avatar_url TEXT NULL;
|
||||
UPDATE users SET raw_avatar_url = avatar_url;
|
||||
|
||||
ALTER TABLE oauth_clients ADD COLUMN raw_icon_url TEXT NULL;
|
||||
UPDATE oauth_clients SET raw_icon_url = icon_url;
|
||||
|
||||
ALTER TABLE organizations ADD COLUMN raw_icon_url TEXT NULL;
|
||||
UPDATE organizations SET raw_icon_url = icon_url;
|
||||
|
||||
ALTER TABLE collections ADD COLUMN raw_icon_url TEXT NULL;
|
||||
UPDATE collections SET raw_icon_url = icon_url;
|
||||
|
||||
ALTER TABLE mods_gallery ADD COLUMN raw_image_url TEXT NULL;
|
||||
UPDATE mods_gallery SET raw_image_url = image_url;
|
||||
ALTER TABLE mods_gallery ALTER COLUMN raw_image_url SET NOT NULL;
|
||||
|
||||
ALTER TABLE uploaded_images ADD COLUMN raw_url TEXT NULL;
|
||||
UPDATE uploaded_images SET raw_url = url;
|
||||
ALTER TABLE uploaded_images ALTER COLUMN raw_url SET NOT NULL;
|
||||
@@ -33,6 +33,7 @@ impl CollectionBuilder {
|
||||
created: Utc::now(),
|
||||
updated: Utc::now(),
|
||||
icon_url: None,
|
||||
raw_icon_url: None,
|
||||
color: None,
|
||||
status: self.status,
|
||||
projects: self.projects,
|
||||
@@ -51,6 +52,7 @@ pub struct Collection {
|
||||
pub created: DateTime<Utc>,
|
||||
pub updated: DateTime<Utc>,
|
||||
pub icon_url: Option<String>,
|
||||
pub raw_icon_url: Option<String>,
|
||||
pub color: Option<u32>,
|
||||
pub status: CollectionStatus,
|
||||
pub projects: Vec<ProjectId>,
|
||||
@@ -65,11 +67,11 @@ impl Collection {
|
||||
"
|
||||
INSERT INTO collections (
|
||||
id, user_id, name, description,
|
||||
created, icon_url, status
|
||||
created, icon_url, raw_icon_url, status
|
||||
)
|
||||
VALUES (
|
||||
$1, $2, $3, $4,
|
||||
$5, $6, $7
|
||||
$5, $6, $7, $8
|
||||
)
|
||||
",
|
||||
self.id as CollectionId,
|
||||
@@ -78,6 +80,7 @@ impl Collection {
|
||||
self.description.as_ref(),
|
||||
self.created,
|
||||
self.icon_url.as_ref(),
|
||||
self.raw_icon_url.as_ref(),
|
||||
self.status.to_string(),
|
||||
)
|
||||
.execute(&mut **transaction)
|
||||
@@ -165,7 +168,7 @@ impl Collection {
|
||||
let collections = sqlx::query!(
|
||||
"
|
||||
SELECT c.id id, c.name name, c.description description,
|
||||
c.icon_url icon_url, c.color color, c.created created, c.user_id user_id,
|
||||
c.icon_url icon_url, c.raw_icon_url raw_icon_url, c.color color, c.created created, c.user_id user_id,
|
||||
c.updated updated, c.status status,
|
||||
ARRAY_AGG(DISTINCT cm.mod_id) filter (where cm.mod_id is not null) mods
|
||||
FROM collections c
|
||||
@@ -183,6 +186,7 @@ impl Collection {
|
||||
name: m.name.clone(),
|
||||
description: m.description.clone(),
|
||||
icon_url: m.icon_url.clone(),
|
||||
raw_icon_url: m.raw_icon_url.clone(),
|
||||
color: m.color.map(|x| x as u32),
|
||||
created: m.created,
|
||||
updated: m.updated,
|
||||
|
||||
@@ -11,6 +11,7 @@ const IMAGES_NAMESPACE: &str = "images";
|
||||
pub struct Image {
|
||||
pub id: ImageId,
|
||||
pub url: String,
|
||||
pub raw_url: String,
|
||||
pub size: u64,
|
||||
pub created: DateTime<Utc>,
|
||||
pub owner_id: UserId,
|
||||
@@ -32,14 +33,15 @@ impl Image {
|
||||
sqlx::query!(
|
||||
"
|
||||
INSERT INTO uploaded_images (
|
||||
id, url, size, created, owner_id, context, mod_id, version_id, thread_message_id, report_id
|
||||
id, url, raw_url, size, created, owner_id, context, mod_id, version_id, thread_message_id, report_id
|
||||
)
|
||||
VALUES (
|
||||
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10
|
||||
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11
|
||||
);
|
||||
",
|
||||
self.id as ImageId,
|
||||
self.url,
|
||||
self.raw_url,
|
||||
self.size as i64,
|
||||
self.created,
|
||||
self.owner_id as UserId,
|
||||
@@ -119,7 +121,7 @@ impl Image {
|
||||
use futures::stream::TryStreamExt;
|
||||
sqlx::query!(
|
||||
"
|
||||
SELECT id, url, size, created, owner_id, context, mod_id, version_id, thread_message_id, report_id
|
||||
SELECT id, url, raw_url, size, created, owner_id, context, mod_id, version_id, thread_message_id, report_id
|
||||
FROM uploaded_images
|
||||
WHERE context = $1
|
||||
AND (mod_id = $2 OR ($2 IS NULL AND mod_id IS NULL))
|
||||
@@ -142,6 +144,7 @@ impl Image {
|
||||
Image {
|
||||
id,
|
||||
url: row.url,
|
||||
raw_url: row.raw_url,
|
||||
size: row.size as u64,
|
||||
created: row.created,
|
||||
owner_id: UserId(row.owner_id),
|
||||
@@ -185,7 +188,7 @@ impl Image {
|
||||
|image_ids| async move {
|
||||
let images = sqlx::query!(
|
||||
"
|
||||
SELECT id, url, size, created, owner_id, context, mod_id, version_id, thread_message_id, report_id
|
||||
SELECT id, url, raw_url, size, created, owner_id, context, mod_id, version_id, thread_message_id, report_id
|
||||
FROM uploaded_images
|
||||
WHERE id = ANY($1)
|
||||
GROUP BY id;
|
||||
@@ -197,6 +200,7 @@ impl Image {
|
||||
let img = Image {
|
||||
id: ImageId(i.id),
|
||||
url: i.url,
|
||||
raw_url: i.raw_url,
|
||||
size: i.size as u64,
|
||||
created: i.created,
|
||||
owner_id: UserId(i.owner_id),
|
||||
|
||||
@@ -18,6 +18,7 @@ pub struct OAuthClient {
|
||||
pub id: OAuthClientId,
|
||||
pub name: String,
|
||||
pub icon_url: Option<String>,
|
||||
pub raw_icon_url: Option<String>,
|
||||
pub max_scopes: Scopes,
|
||||
pub secret_hash: String,
|
||||
pub redirect_uris: Vec<OAuthRedirectUri>,
|
||||
@@ -31,6 +32,7 @@ struct ClientQueryResult {
|
||||
id: i64,
|
||||
name: String,
|
||||
icon_url: Option<String>,
|
||||
raw_icon_url: Option<String>,
|
||||
max_scopes: i64,
|
||||
secret_hash: String,
|
||||
created: DateTime<Utc>,
|
||||
@@ -53,6 +55,7 @@ macro_rules! select_clients_with_predicate {
|
||||
clients.id as "id!",
|
||||
clients.name as "name!",
|
||||
clients.icon_url as "icon_url?",
|
||||
clients.raw_icon_url as "raw_icon_url?",
|
||||
clients.max_scopes as "max_scopes!",
|
||||
clients.secret_hash as "secret_hash!",
|
||||
clients.created as "created!",
|
||||
@@ -133,15 +136,16 @@ impl OAuthClient {
|
||||
sqlx::query!(
|
||||
"
|
||||
INSERT INTO oauth_clients (
|
||||
id, name, icon_url, max_scopes, secret_hash, created_by
|
||||
id, name, icon_url, raw_icon_url, max_scopes, secret_hash, created_by
|
||||
)
|
||||
VALUES (
|
||||
$1, $2, $3, $4, $5, $6
|
||||
$1, $2, $3, $4, $5, $6, $7
|
||||
)
|
||||
",
|
||||
self.id.0,
|
||||
self.name,
|
||||
self.icon_url,
|
||||
self.raw_icon_url,
|
||||
self.max_scopes.to_postgres(),
|
||||
self.secret_hash,
|
||||
self.created_by.0
|
||||
@@ -161,11 +165,12 @@ impl OAuthClient {
|
||||
sqlx::query!(
|
||||
"
|
||||
UPDATE oauth_clients
|
||||
SET name = $1, icon_url = $2, max_scopes = $3, url = $4, description = $5
|
||||
WHERE (id = $6)
|
||||
SET name = $1, icon_url = $2, raw_icon_url = $3, max_scopes = $4, url = $5, description = $6
|
||||
WHERE (id = $7)
|
||||
",
|
||||
self.name,
|
||||
self.icon_url,
|
||||
self.raw_icon_url,
|
||||
self.max_scopes.to_postgres(),
|
||||
self.url,
|
||||
self.description,
|
||||
@@ -243,6 +248,7 @@ impl From<ClientQueryResult> for OAuthClient {
|
||||
id: OAuthClientId(r.id),
|
||||
name: r.name,
|
||||
icon_url: r.icon_url,
|
||||
raw_icon_url: r.raw_icon_url,
|
||||
max_scopes: Scopes::from_postgres(r.max_scopes),
|
||||
secret_hash: r.secret_hash,
|
||||
redirect_uris: redirects,
|
||||
|
||||
@@ -30,6 +30,7 @@ pub struct Organization {
|
||||
|
||||
/// The display icon for the organization
|
||||
pub icon_url: Option<String>,
|
||||
pub raw_icon_url: Option<String>,
|
||||
pub color: Option<u32>,
|
||||
}
|
||||
|
||||
@@ -40,8 +41,8 @@ impl Organization {
|
||||
) -> Result<(), super::DatabaseError> {
|
||||
sqlx::query!(
|
||||
"
|
||||
INSERT INTO organizations (id, slug, name, team_id, description, icon_url, color)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
||||
INSERT INTO organizations (id, slug, name, team_id, description, icon_url, raw_icon_url, color)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
|
||||
",
|
||||
self.id.0,
|
||||
self.slug,
|
||||
@@ -49,6 +50,7 @@ impl Organization {
|
||||
self.team_id as TeamId,
|
||||
self.description,
|
||||
self.icon_url,
|
||||
self.raw_icon_url,
|
||||
self.color.map(|x| x as i32),
|
||||
)
|
||||
.execute(&mut **transaction)
|
||||
@@ -125,7 +127,7 @@ impl Organization {
|
||||
|
||||
let organizations = sqlx::query!(
|
||||
"
|
||||
SELECT o.id, o.slug, o.name, o.team_id, o.description, o.icon_url, o.color
|
||||
SELECT o.id, o.slug, o.name, o.team_id, o.description, o.icon_url, o.raw_icon_url, o.color
|
||||
FROM organizations o
|
||||
WHERE o.id = ANY($1) OR LOWER(o.slug) = ANY($2)
|
||||
GROUP BY o.id;
|
||||
@@ -142,6 +144,7 @@ impl Organization {
|
||||
team_id: TeamId(m.team_id),
|
||||
description: m.description,
|
||||
icon_url: m.icon_url,
|
||||
raw_icon_url: m.raw_icon_url,
|
||||
color: m.color.map(|x| x as u32),
|
||||
};
|
||||
|
||||
@@ -168,7 +171,7 @@ impl Organization {
|
||||
{
|
||||
let result = sqlx::query!(
|
||||
"
|
||||
SELECT o.id, o.slug, o.name, o.team_id, o.description, o.icon_url, o.color
|
||||
SELECT o.id, o.slug, o.name, o.team_id, o.description, o.icon_url, o.raw_icon_url, o.color
|
||||
FROM organizations o
|
||||
LEFT JOIN mods m ON m.organization_id = o.id
|
||||
WHERE m.id = $1
|
||||
@@ -187,6 +190,7 @@ impl Organization {
|
||||
team_id: TeamId(result.team_id),
|
||||
description: result.description,
|
||||
icon_url: result.icon_url,
|
||||
raw_icon_url: result.raw_icon_url,
|
||||
color: result.color.map(|x| x as u32),
|
||||
}))
|
||||
} else {
|
||||
|
||||
@@ -58,6 +58,7 @@ impl LinkUrl {
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct GalleryItem {
|
||||
pub image_url: String,
|
||||
pub raw_image_url: String,
|
||||
pub featured: bool,
|
||||
pub name: Option<String>,
|
||||
pub description: Option<String>,
|
||||
@@ -71,7 +72,8 @@ impl GalleryItem {
|
||||
project_id: ProjectId,
|
||||
transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
|
||||
) -> Result<(), sqlx::error::Error> {
|
||||
let (project_ids, image_urls, featureds, names, descriptions, orderings): (
|
||||
let (project_ids, image_urls, raw_image_urls, featureds, names, descriptions, orderings): (
|
||||
Vec<_>,
|
||||
Vec<_>,
|
||||
Vec<_>,
|
||||
Vec<_>,
|
||||
@@ -84,6 +86,7 @@ impl GalleryItem {
|
||||
(
|
||||
project_id.0,
|
||||
gi.image_url,
|
||||
gi.raw_image_url,
|
||||
gi.featured,
|
||||
gi.name,
|
||||
gi.description,
|
||||
@@ -94,12 +97,13 @@ impl GalleryItem {
|
||||
sqlx::query!(
|
||||
"
|
||||
INSERT INTO mods_gallery (
|
||||
mod_id, image_url, featured, name, description, ordering
|
||||
mod_id, image_url, raw_image_url, featured, name, description, ordering
|
||||
)
|
||||
SELECT * FROM UNNEST ($1::bigint[], $2::varchar[], $3::bool[], $4::varchar[], $5::varchar[], $6::bigint[])
|
||||
SELECT * FROM UNNEST ($1::bigint[], $2::varchar[], $3::varchar[], $4::bool[], $5::varchar[], $6::varchar[], $7::bigint[])
|
||||
",
|
||||
&project_ids[..],
|
||||
&image_urls[..],
|
||||
&raw_image_urls[..],
|
||||
&featureds[..],
|
||||
&names[..] as &[Option<String>],
|
||||
&descriptions[..] as &[Option<String>],
|
||||
@@ -153,6 +157,7 @@ pub struct ProjectBuilder {
|
||||
pub summary: String,
|
||||
pub description: String,
|
||||
pub icon_url: Option<String>,
|
||||
pub raw_icon_url: Option<String>,
|
||||
pub license_url: Option<String>,
|
||||
pub categories: Vec<CategoryId>,
|
||||
pub additional_categories: Vec<CategoryId>,
|
||||
@@ -192,6 +197,7 @@ impl ProjectBuilder {
|
||||
downloads: 0,
|
||||
follows: 0,
|
||||
icon_url: self.icon_url,
|
||||
raw_icon_url: self.raw_icon_url,
|
||||
license_url: self.license_url,
|
||||
license: self.license,
|
||||
slug: self.slug,
|
||||
@@ -253,6 +259,7 @@ pub struct Project {
|
||||
pub downloads: i32,
|
||||
pub follows: i32,
|
||||
pub icon_url: Option<String>,
|
||||
pub raw_icon_url: Option<String>,
|
||||
pub license_url: Option<String>,
|
||||
pub license: String,
|
||||
pub slug: Option<String>,
|
||||
@@ -273,15 +280,15 @@ impl Project {
|
||||
"
|
||||
INSERT INTO mods (
|
||||
id, team_id, name, summary, description,
|
||||
published, downloads, icon_url, status, requested_status,
|
||||
published, downloads, icon_url, raw_icon_url, status, requested_status,
|
||||
license_url, license,
|
||||
slug, color, monetization_status, organization_id
|
||||
)
|
||||
VALUES (
|
||||
$1, $2, $3, $4, $5, $6,
|
||||
$7, $8, $9, $10,
|
||||
$11, $12,
|
||||
LOWER($13), $14, $15, $16
|
||||
$7, $8, $9, $10, $11,
|
||||
$12, $13,
|
||||
LOWER($14), $15, $16, $17
|
||||
)
|
||||
",
|
||||
self.id as ProjectId,
|
||||
@@ -292,6 +299,7 @@ impl Project {
|
||||
self.published,
|
||||
self.downloads,
|
||||
self.icon_url.as_ref(),
|
||||
self.raw_icon_url.as_ref(),
|
||||
self.status.as_str(),
|
||||
self.requested_status.map(|x| x.as_str()),
|
||||
self.license_url.as_ref(),
|
||||
@@ -620,7 +628,7 @@ impl Project {
|
||||
|
||||
let mods_gallery: DashMap<ProjectId, Vec<GalleryItem>> = sqlx::query!(
|
||||
"
|
||||
SELECT DISTINCT mod_id, mg.image_url, mg.featured, mg.name, mg.description, mg.created, mg.ordering
|
||||
SELECT DISTINCT mod_id, mg.image_url, mg.raw_image_url, mg.featured, mg.name, mg.description, mg.created, mg.ordering
|
||||
FROM mods_gallery mg
|
||||
INNER JOIN mods m ON mg.mod_id = m.id
|
||||
WHERE m.id = ANY($1) OR m.slug = ANY($2)
|
||||
@@ -633,6 +641,7 @@ impl Project {
|
||||
.or_default()
|
||||
.push(GalleryItem {
|
||||
image_url: m.image_url,
|
||||
raw_image_url: m.raw_image_url,
|
||||
featured: m.featured.unwrap_or(false),
|
||||
name: m.name,
|
||||
description: m.description,
|
||||
@@ -742,7 +751,7 @@ impl Project {
|
||||
let projects = sqlx::query!(
|
||||
"
|
||||
SELECT m.id id, m.name name, m.summary summary, m.downloads downloads, m.follows follows,
|
||||
m.icon_url icon_url, m.description description, m.published published,
|
||||
m.icon_url icon_url, m.raw_icon_url raw_icon_url, m.description description, m.published published,
|
||||
m.updated updated, m.approved approved, m.queued, m.status status, m.requested_status requested_status,
|
||||
m.license_url license_url,
|
||||
m.team_id team_id, m.organization_id organization_id, m.license license, m.slug slug, m.moderation_message moderation_message, m.moderation_message_body moderation_message_body,
|
||||
@@ -788,6 +797,7 @@ impl Project {
|
||||
summary: m.summary.clone(),
|
||||
downloads: m.downloads,
|
||||
icon_url: m.icon_url.clone(),
|
||||
raw_icon_url: m.raw_icon_url.clone(),
|
||||
published: m.published,
|
||||
updated: m.updated,
|
||||
license_url: m.license_url.clone(),
|
||||
|
||||
@@ -40,6 +40,7 @@ pub struct User {
|
||||
pub email: Option<String>,
|
||||
pub email_verified: bool,
|
||||
pub avatar_url: Option<String>,
|
||||
pub raw_avatar_url: Option<String>,
|
||||
pub bio: Option<String>,
|
||||
pub created: DateTime<Utc>,
|
||||
pub role: String,
|
||||
@@ -57,7 +58,7 @@ impl User {
|
||||
"
|
||||
INSERT INTO users (
|
||||
id, username, email,
|
||||
avatar_url, bio, created,
|
||||
avatar_url, raw_avatar_url, bio, created,
|
||||
github_id, discord_id, gitlab_id, google_id, steam_id, microsoft_id,
|
||||
email_verified, password, paypal_id, paypal_country, paypal_email,
|
||||
venmo_handle, stripe_customer_id
|
||||
@@ -66,13 +67,14 @@ impl User {
|
||||
$1, $2, $3, $4, $5,
|
||||
$6, $7,
|
||||
$8, $9, $10, $11, $12, $13,
|
||||
$14, $15, $16, $17, $18, $19
|
||||
$14, $15, $16, $17, $18, $19, $20
|
||||
)
|
||||
",
|
||||
self.id as UserId,
|
||||
&self.username,
|
||||
self.email.as_ref(),
|
||||
self.avatar_url.as_ref(),
|
||||
self.raw_avatar_url.as_ref(),
|
||||
self.bio.as_ref(),
|
||||
self.created,
|
||||
self.github_id,
|
||||
@@ -165,7 +167,7 @@ impl User {
|
||||
let users = sqlx::query!(
|
||||
"
|
||||
SELECT id, email,
|
||||
avatar_url, username, bio,
|
||||
avatar_url, raw_avatar_url, username, bio,
|
||||
created, role, badges,
|
||||
balance,
|
||||
github_id, discord_id, gitlab_id, google_id, steam_id, microsoft_id,
|
||||
@@ -190,6 +192,7 @@ impl User {
|
||||
email: u.email,
|
||||
email_verified: u.email_verified,
|
||||
avatar_url: u.avatar_url,
|
||||
raw_avatar_url: u.raw_avatar_url,
|
||||
username: u.username.clone(),
|
||||
bio: u.bio,
|
||||
created: u.created,
|
||||
|
||||
@@ -357,6 +357,7 @@ impl From<Version> for LegacyVersion {
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
pub struct LegacyGalleryItem {
|
||||
pub url: String,
|
||||
pub raw_url: String,
|
||||
pub featured: bool,
|
||||
pub title: Option<String>,
|
||||
pub description: Option<String>,
|
||||
@@ -368,6 +369,7 @@ impl LegacyGalleryItem {
|
||||
fn from(data: crate::models::projects::GalleryItem) -> Self {
|
||||
Self {
|
||||
url: data.url,
|
||||
raw_url: data.raw_url,
|
||||
featured: data.featured,
|
||||
title: data.name,
|
||||
description: data.description,
|
||||
|
||||
@@ -215,6 +215,7 @@ impl From<QueryProject> for Project {
|
||||
.into_iter()
|
||||
.map(|x| GalleryItem {
|
||||
url: x.image_url,
|
||||
raw_url: x.raw_image_url,
|
||||
featured: x.featured,
|
||||
name: x.name,
|
||||
description: x.description,
|
||||
@@ -387,6 +388,7 @@ impl Project {
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
pub struct GalleryItem {
|
||||
pub url: String,
|
||||
pub raw_url: String,
|
||||
pub featured: bool,
|
||||
pub name: Option<String>,
|
||||
pub description: Option<String>,
|
||||
|
||||
@@ -14,7 +14,8 @@ use crate::routes::internal::session::issue_session;
|
||||
use crate::routes::ApiError;
|
||||
use crate::util::captcha::check_turnstile_captcha;
|
||||
use crate::util::env::parse_strings_from_var;
|
||||
use crate::util::ext::{get_image_content_type, get_image_ext};
|
||||
use crate::util::ext::get_image_ext;
|
||||
use crate::util::img::upload_image_optimized;
|
||||
use crate::util::validate::{validation_errors_to_string, RE_URL_SAFE};
|
||||
use actix_web::web::{scope, Data, Payload, Query, ServiceConfig};
|
||||
use actix_web::{delete, get, patch, post, web, HttpRequest, HttpResponse};
|
||||
@@ -112,9 +113,7 @@ impl TempUser {
|
||||
}
|
||||
}
|
||||
|
||||
let avatar_url = if let Some(avatar_url) = self.avatar_url {
|
||||
let cdn_url = dotenvy::var("CDN_URL")?;
|
||||
|
||||
let (avatar_url, raw_avatar_url) = if let Some(avatar_url) = self.avatar_url {
|
||||
let res = reqwest::get(&avatar_url).await?;
|
||||
let headers = res.headers().clone();
|
||||
|
||||
@@ -122,36 +121,34 @@ impl TempUser {
|
||||
.get(reqwest::header::CONTENT_TYPE)
|
||||
.and_then(|ct| ct.to_str().ok())
|
||||
{
|
||||
get_image_ext(content_type).map(|ext| (ext, content_type))
|
||||
} else if let Some(ext) = avatar_url.rsplit('.').next() {
|
||||
get_image_content_type(ext).map(|content_type| (ext, content_type))
|
||||
get_image_ext(content_type)
|
||||
} else {
|
||||
None
|
||||
avatar_url.rsplit('.').next()
|
||||
};
|
||||
|
||||
if let Some((ext, content_type)) = img_data {
|
||||
if let Some(ext) = img_data {
|
||||
let bytes = res.bytes().await?;
|
||||
let hash = sha1::Sha1::from(&bytes).hexdigest();
|
||||
|
||||
let upload_data = file_host
|
||||
.upload_file(
|
||||
content_type,
|
||||
&format!(
|
||||
"user/{}/{}.{}",
|
||||
crate::models::users::UserId::from(user_id),
|
||||
hash,
|
||||
ext
|
||||
),
|
||||
bytes,
|
||||
)
|
||||
.await?;
|
||||
let upload_result = upload_image_optimized(
|
||||
&format!("user/{}", crate::models::users::UserId::from(user_id)),
|
||||
bytes,
|
||||
ext,
|
||||
Some(96),
|
||||
Some(1.0),
|
||||
&**file_host,
|
||||
)
|
||||
.await;
|
||||
|
||||
Some(format!("{}/{}", cdn_url, upload_data.file_name))
|
||||
if let Ok(upload_result) = upload_result {
|
||||
(Some(upload_result.url), Some(upload_result.raw_url))
|
||||
} else {
|
||||
(None, None)
|
||||
}
|
||||
} else {
|
||||
None
|
||||
(None, None)
|
||||
}
|
||||
} else {
|
||||
None
|
||||
(None, None)
|
||||
};
|
||||
|
||||
if let Some(username) = username {
|
||||
@@ -223,6 +220,7 @@ impl TempUser {
|
||||
email: self.email,
|
||||
email_verified: true,
|
||||
avatar_url,
|
||||
raw_avatar_url,
|
||||
bio: self.bio,
|
||||
created: Utc::now(),
|
||||
role: Role::Developer.to_string(),
|
||||
@@ -1518,6 +1516,7 @@ pub async fn create_account_with_password(
|
||||
email: Some(new_account.email.clone()),
|
||||
email_verified: false,
|
||||
avatar_url: None,
|
||||
raw_avatar_url: None,
|
||||
bio: None,
|
||||
created: Utc::now(),
|
||||
role: Role::Developer.to_string(),
|
||||
|
||||
@@ -10,6 +10,7 @@ use crate::models::pats::Scopes;
|
||||
use crate::queue::session::AuthQueue;
|
||||
use crate::routes::v3::project_creation::CreateError;
|
||||
use crate::routes::ApiError;
|
||||
use crate::util::img::delete_old_images;
|
||||
use crate::util::routes::read_from_payload;
|
||||
use crate::util::validate::validation_errors_to_string;
|
||||
use crate::{database, models};
|
||||
@@ -371,78 +372,69 @@ pub async fn collection_icon_edit(
|
||||
mut payload: web::Payload,
|
||||
session_queue: web::Data<AuthQueue>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
if let Some(content_type) = crate::util::ext::get_image_content_type(&ext.ext) {
|
||||
let cdn_url = dotenvy::var("CDN_URL")?;
|
||||
let user = get_user_from_headers(
|
||||
&req,
|
||||
&**pool,
|
||||
&redis,
|
||||
&session_queue,
|
||||
Some(&[Scopes::COLLECTION_WRITE]),
|
||||
)
|
||||
let user = get_user_from_headers(
|
||||
&req,
|
||||
&**pool,
|
||||
&redis,
|
||||
&session_queue,
|
||||
Some(&[Scopes::COLLECTION_WRITE]),
|
||||
)
|
||||
.await?
|
||||
.1;
|
||||
|
||||
let string = info.into_inner().0;
|
||||
let id = database::models::CollectionId(parse_base62(&string)? as i64);
|
||||
let collection_item = database::models::Collection::get(id, &**pool, &redis)
|
||||
.await?
|
||||
.1;
|
||||
.ok_or_else(|| {
|
||||
ApiError::InvalidInput("The specified collection does not exist!".to_string())
|
||||
})?;
|
||||
|
||||
let string = info.into_inner().0;
|
||||
let id = database::models::CollectionId(parse_base62(&string)? as i64);
|
||||
let collection_item = database::models::Collection::get(id, &**pool, &redis)
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
ApiError::InvalidInput("The specified collection does not exist!".to_string())
|
||||
})?;
|
||||
|
||||
if !can_modify_collection(&collection_item, &user) {
|
||||
return Ok(HttpResponse::Unauthorized().body(""));
|
||||
}
|
||||
|
||||
if let Some(icon) = collection_item.icon_url {
|
||||
let name = icon.split(&format!("{cdn_url}/")).nth(1);
|
||||
|
||||
if let Some(icon_path) = name {
|
||||
file_host.delete_file_version("", icon_path).await?;
|
||||
}
|
||||
}
|
||||
|
||||
let bytes =
|
||||
read_from_payload(&mut payload, 262144, "Icons must be smaller than 256KiB").await?;
|
||||
|
||||
let color = crate::util::img::get_color_from_img(&bytes)?;
|
||||
|
||||
let hash = sha1::Sha1::from(&bytes).hexdigest();
|
||||
let collection_id: CollectionId = collection_item.id.into();
|
||||
let upload_data = file_host
|
||||
.upload_file(
|
||||
content_type,
|
||||
&format!("data/{}/{}.{}", collection_id, hash, ext.ext),
|
||||
bytes.freeze(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let mut transaction = pool.begin().await?;
|
||||
|
||||
sqlx::query!(
|
||||
"
|
||||
UPDATE collections
|
||||
SET icon_url = $1, color = $2
|
||||
WHERE (id = $3)
|
||||
",
|
||||
format!("{}/{}", cdn_url, upload_data.file_name),
|
||||
color.map(|x| x as i32),
|
||||
collection_item.id as database::models::ids::CollectionId,
|
||||
)
|
||||
.execute(&mut *transaction)
|
||||
.await?;
|
||||
|
||||
transaction.commit().await?;
|
||||
database::models::Collection::clear_cache(collection_item.id, &redis).await?;
|
||||
|
||||
Ok(HttpResponse::NoContent().body(""))
|
||||
} else {
|
||||
Err(ApiError::InvalidInput(format!(
|
||||
"Invalid format for collection icon: {}",
|
||||
ext.ext
|
||||
)))
|
||||
if !can_modify_collection(&collection_item, &user) {
|
||||
return Ok(HttpResponse::Unauthorized().body(""));
|
||||
}
|
||||
|
||||
delete_old_images(
|
||||
collection_item.icon_url,
|
||||
collection_item.raw_icon_url,
|
||||
&***file_host,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let bytes =
|
||||
read_from_payload(&mut payload, 262144, "Icons must be smaller than 256KiB").await?;
|
||||
|
||||
let collection_id: CollectionId = collection_item.id.into();
|
||||
let upload_result = crate::util::img::upload_image_optimized(
|
||||
&format!("data/{}", collection_id),
|
||||
bytes.freeze(),
|
||||
&ext.ext,
|
||||
Some(96),
|
||||
Some(1.0),
|
||||
&***file_host,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let mut transaction = pool.begin().await?;
|
||||
|
||||
sqlx::query!(
|
||||
"
|
||||
UPDATE collections
|
||||
SET icon_url = $1, raw_icon_url = $2, color = $3
|
||||
WHERE (id = $4)
|
||||
",
|
||||
upload_result.url,
|
||||
upload_result.raw_url,
|
||||
upload_result.color.map(|x| x as i32),
|
||||
collection_item.id as database::models::ids::CollectionId,
|
||||
)
|
||||
.execute(&mut *transaction)
|
||||
.await?;
|
||||
|
||||
transaction.commit().await?;
|
||||
database::models::Collection::clear_cache(collection_item.id, &redis).await?;
|
||||
|
||||
Ok(HttpResponse::NoContent().body(""))
|
||||
}
|
||||
|
||||
pub async fn delete_collection_icon(
|
||||
@@ -474,21 +466,18 @@ pub async fn delete_collection_icon(
|
||||
return Ok(HttpResponse::Unauthorized().body(""));
|
||||
}
|
||||
|
||||
let cdn_url = dotenvy::var("CDN_URL")?;
|
||||
if let Some(icon) = collection_item.icon_url {
|
||||
let name = icon.split(&format!("{cdn_url}/")).nth(1);
|
||||
|
||||
if let Some(icon_path) = name {
|
||||
file_host.delete_file_version("", icon_path).await?;
|
||||
}
|
||||
}
|
||||
|
||||
delete_old_images(
|
||||
collection_item.icon_url,
|
||||
collection_item.raw_icon_url,
|
||||
&***file_host,
|
||||
)
|
||||
.await?;
|
||||
let mut transaction = pool.begin().await?;
|
||||
|
||||
sqlx::query!(
|
||||
"
|
||||
UPDATE collections
|
||||
SET icon_url = NULL, color = NULL
|
||||
SET icon_url = NULL, raw_icon_url = NULL, color = NULL
|
||||
WHERE (id = $1)
|
||||
",
|
||||
collection_item.id as database::models::ids::CollectionId,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use super::threads::is_authorized_thread;
|
||||
use crate::auth::checks::{is_team_member_project, is_team_member_version};
|
||||
use crate::auth::get_user_from_headers;
|
||||
use crate::database;
|
||||
@@ -11,13 +12,12 @@ use crate::models::images::{Image, ImageContext};
|
||||
use crate::models::reports::ReportId;
|
||||
use crate::queue::session::AuthQueue;
|
||||
use crate::routes::ApiError;
|
||||
use crate::util::img::upload_image_optimized;
|
||||
use crate::util::routes::read_from_payload;
|
||||
use actix_web::{web, HttpRequest, HttpResponse};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::PgPool;
|
||||
|
||||
use super::threads::is_authorized_thread;
|
||||
|
||||
pub fn config(cfg: &mut web::ServiceConfig) {
|
||||
cfg.route("image", web::post().to(images_add));
|
||||
}
|
||||
@@ -46,198 +46,182 @@ pub async fn images_add(
|
||||
redis: web::Data<RedisPool>,
|
||||
session_queue: web::Data<AuthQueue>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
if let Some(content_type) = crate::util::ext::get_image_content_type(&data.ext) {
|
||||
let mut context = ImageContext::from_str(&data.context, None);
|
||||
let mut context = ImageContext::from_str(&data.context, None);
|
||||
|
||||
let scopes = vec![context.relevant_scope()];
|
||||
let scopes = vec![context.relevant_scope()];
|
||||
|
||||
let cdn_url = dotenvy::var("CDN_URL")?;
|
||||
let user = get_user_from_headers(&req, &**pool, &redis, &session_queue, Some(&scopes))
|
||||
.await?
|
||||
.1;
|
||||
let user = get_user_from_headers(&req, &**pool, &redis, &session_queue, Some(&scopes))
|
||||
.await?
|
||||
.1;
|
||||
|
||||
// Attempt to associated a supplied id with the context
|
||||
// If the context cannot be found, or the user is not authorized to upload images for the context, return an error
|
||||
match &mut context {
|
||||
ImageContext::Project { project_id } => {
|
||||
if let Some(id) = data.project_id {
|
||||
let project = project_item::Project::get(&id, &**pool, &redis).await?;
|
||||
if let Some(project) = project {
|
||||
if is_team_member_project(&project.inner, &Some(user.clone()), &pool)
|
||||
.await?
|
||||
{
|
||||
*project_id = Some(project.inner.id.into());
|
||||
} else {
|
||||
return Err(ApiError::CustomAuthentication(
|
||||
"You are not authorized to upload images for this project"
|
||||
.to_string(),
|
||||
));
|
||||
}
|
||||
} else {
|
||||
return Err(ApiError::InvalidInput(
|
||||
"The project could not be found.".to_string(),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
ImageContext::Version { version_id } => {
|
||||
if let Some(id) = data.version_id {
|
||||
let version = version_item::Version::get(id.into(), &**pool, &redis).await?;
|
||||
if let Some(version) = version {
|
||||
if is_team_member_version(
|
||||
&version.inner,
|
||||
&Some(user.clone()),
|
||||
&pool,
|
||||
&redis,
|
||||
)
|
||||
.await?
|
||||
{
|
||||
*version_id = Some(version.inner.id.into());
|
||||
} else {
|
||||
return Err(ApiError::CustomAuthentication(
|
||||
"You are not authorized to upload images for this version"
|
||||
.to_string(),
|
||||
));
|
||||
}
|
||||
} else {
|
||||
return Err(ApiError::InvalidInput(
|
||||
"The version could not be found.".to_string(),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
ImageContext::ThreadMessage { thread_message_id } => {
|
||||
if let Some(id) = data.thread_message_id {
|
||||
let thread_message = thread_item::ThreadMessage::get(id.into(), &**pool)
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
ApiError::InvalidInput(
|
||||
"The thread message could not found.".to_string(),
|
||||
)
|
||||
})?;
|
||||
let thread = thread_item::Thread::get(thread_message.thread_id, &**pool)
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
ApiError::InvalidInput(
|
||||
"The thread associated with the thread message could not be found"
|
||||
.to_string(),
|
||||
)
|
||||
})?;
|
||||
if is_authorized_thread(&thread, &user, &pool).await? {
|
||||
*thread_message_id = Some(thread_message.id.into());
|
||||
// Attempt to associated a supplied id with the context
|
||||
// If the context cannot be found, or the user is not authorized to upload images for the context, return an error
|
||||
match &mut context {
|
||||
ImageContext::Project { project_id } => {
|
||||
if let Some(id) = data.project_id {
|
||||
let project = project_item::Project::get(&id, &**pool, &redis).await?;
|
||||
if let Some(project) = project {
|
||||
if is_team_member_project(&project.inner, &Some(user.clone()), &pool).await? {
|
||||
*project_id = Some(project.inner.id.into());
|
||||
} else {
|
||||
return Err(ApiError::CustomAuthentication(
|
||||
"You are not authorized to upload images for this thread message"
|
||||
.to_string(),
|
||||
"You are not authorized to upload images for this project".to_string(),
|
||||
));
|
||||
}
|
||||
} else {
|
||||
return Err(ApiError::InvalidInput(
|
||||
"The project could not be found.".to_string(),
|
||||
));
|
||||
}
|
||||
}
|
||||
ImageContext::Report { report_id } => {
|
||||
if let Some(id) = data.report_id {
|
||||
let report = report_item::Report::get(id.into(), &**pool)
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
ApiError::InvalidInput("The report could not be found.".to_string())
|
||||
})?;
|
||||
let thread = thread_item::Thread::get(report.thread_id, &**pool)
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
ApiError::InvalidInput(
|
||||
"The thread associated with the report could not be found."
|
||||
.to_string(),
|
||||
)
|
||||
})?;
|
||||
if is_authorized_thread(&thread, &user, &pool).await? {
|
||||
*report_id = Some(report.id.into());
|
||||
} else {
|
||||
return Err(ApiError::CustomAuthentication(
|
||||
"You are not authorized to upload images for this report".to_string(),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
ImageContext::Unknown => {
|
||||
return Err(ApiError::InvalidInput(
|
||||
"Context must be one of: project, version, thread_message, report".to_string(),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
// Upload the image to the file host
|
||||
let bytes =
|
||||
read_from_payload(&mut payload, 1_048_576, "Icons must be smaller than 1MiB").await?;
|
||||
|
||||
let hash = sha1::Sha1::from(&bytes).hexdigest();
|
||||
let upload_data = file_host
|
||||
.upload_file(
|
||||
content_type,
|
||||
&format!("data/cached_images/{}.{}", hash, data.ext),
|
||||
bytes.freeze(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let mut transaction = pool.begin().await?;
|
||||
|
||||
let db_image: database::models::Image = database::models::Image {
|
||||
id: database::models::generate_image_id(&mut transaction).await?,
|
||||
url: format!("{}/{}", cdn_url, upload_data.file_name),
|
||||
size: upload_data.content_length as u64,
|
||||
created: chrono::Utc::now(),
|
||||
owner_id: database::models::UserId::from(user.id),
|
||||
context: context.context_as_str().to_string(),
|
||||
project_id: if let ImageContext::Project {
|
||||
project_id: Some(id),
|
||||
} = context
|
||||
{
|
||||
Some(database::models::ProjectId::from(id))
|
||||
} else {
|
||||
None
|
||||
},
|
||||
version_id: if let ImageContext::Version {
|
||||
version_id: Some(id),
|
||||
} = context
|
||||
{
|
||||
Some(database::models::VersionId::from(id))
|
||||
} else {
|
||||
None
|
||||
},
|
||||
thread_message_id: if let ImageContext::ThreadMessage {
|
||||
thread_message_id: Some(id),
|
||||
} = context
|
||||
{
|
||||
Some(database::models::ThreadMessageId::from(id))
|
||||
} else {
|
||||
None
|
||||
},
|
||||
report_id: if let ImageContext::Report {
|
||||
report_id: Some(id),
|
||||
} = context
|
||||
{
|
||||
Some(database::models::ReportId::from(id))
|
||||
} else {
|
||||
None
|
||||
},
|
||||
};
|
||||
|
||||
// Insert
|
||||
db_image.insert(&mut transaction).await?;
|
||||
|
||||
let image = Image {
|
||||
id: db_image.id.into(),
|
||||
url: db_image.url,
|
||||
size: db_image.size,
|
||||
created: db_image.created,
|
||||
owner_id: db_image.owner_id.into(),
|
||||
context,
|
||||
};
|
||||
|
||||
transaction.commit().await?;
|
||||
|
||||
Ok(HttpResponse::Ok().json(image))
|
||||
} else {
|
||||
Err(ApiError::InvalidInput(
|
||||
"The specified file is not an image!".to_string(),
|
||||
))
|
||||
ImageContext::Version { version_id } => {
|
||||
if let Some(id) = data.version_id {
|
||||
let version = version_item::Version::get(id.into(), &**pool, &redis).await?;
|
||||
if let Some(version) = version {
|
||||
if is_team_member_version(&version.inner, &Some(user.clone()), &pool, &redis)
|
||||
.await?
|
||||
{
|
||||
*version_id = Some(version.inner.id.into());
|
||||
} else {
|
||||
return Err(ApiError::CustomAuthentication(
|
||||
"You are not authorized to upload images for this version".to_string(),
|
||||
));
|
||||
}
|
||||
} else {
|
||||
return Err(ApiError::InvalidInput(
|
||||
"The version could not be found.".to_string(),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
ImageContext::ThreadMessage { thread_message_id } => {
|
||||
if let Some(id) = data.thread_message_id {
|
||||
let thread_message = thread_item::ThreadMessage::get(id.into(), &**pool)
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
ApiError::InvalidInput("The thread message could not found.".to_string())
|
||||
})?;
|
||||
let thread = thread_item::Thread::get(thread_message.thread_id, &**pool)
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
ApiError::InvalidInput(
|
||||
"The thread associated with the thread message could not be found"
|
||||
.to_string(),
|
||||
)
|
||||
})?;
|
||||
if is_authorized_thread(&thread, &user, &pool).await? {
|
||||
*thread_message_id = Some(thread_message.id.into());
|
||||
} else {
|
||||
return Err(ApiError::CustomAuthentication(
|
||||
"You are not authorized to upload images for this thread message"
|
||||
.to_string(),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
ImageContext::Report { report_id } => {
|
||||
if let Some(id) = data.report_id {
|
||||
let report = report_item::Report::get(id.into(), &**pool)
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
ApiError::InvalidInput("The report could not be found.".to_string())
|
||||
})?;
|
||||
let thread = thread_item::Thread::get(report.thread_id, &**pool)
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
ApiError::InvalidInput(
|
||||
"The thread associated with the report could not be found.".to_string(),
|
||||
)
|
||||
})?;
|
||||
if is_authorized_thread(&thread, &user, &pool).await? {
|
||||
*report_id = Some(report.id.into());
|
||||
} else {
|
||||
return Err(ApiError::CustomAuthentication(
|
||||
"You are not authorized to upload images for this report".to_string(),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
ImageContext::Unknown => {
|
||||
return Err(ApiError::InvalidInput(
|
||||
"Context must be one of: project, version, thread_message, report".to_string(),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
// Upload the image to the file host
|
||||
let bytes =
|
||||
read_from_payload(&mut payload, 1_048_576, "Icons must be smaller than 1MiB").await?;
|
||||
|
||||
let content_length = bytes.len();
|
||||
let upload_result = upload_image_optimized(
|
||||
"data/cached_images",
|
||||
bytes.freeze(),
|
||||
&data.ext,
|
||||
None,
|
||||
None,
|
||||
&***file_host,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let mut transaction = pool.begin().await?;
|
||||
|
||||
let db_image: database::models::Image = database::models::Image {
|
||||
id: database::models::generate_image_id(&mut transaction).await?,
|
||||
url: upload_result.url,
|
||||
raw_url: upload_result.raw_url,
|
||||
size: content_length as u64,
|
||||
created: chrono::Utc::now(),
|
||||
owner_id: database::models::UserId::from(user.id),
|
||||
context: context.context_as_str().to_string(),
|
||||
project_id: if let ImageContext::Project {
|
||||
project_id: Some(id),
|
||||
} = context
|
||||
{
|
||||
Some(crate::database::models::ProjectId::from(id))
|
||||
} else {
|
||||
None
|
||||
},
|
||||
version_id: if let ImageContext::Version {
|
||||
version_id: Some(id),
|
||||
} = context
|
||||
{
|
||||
Some(database::models::VersionId::from(id))
|
||||
} else {
|
||||
None
|
||||
},
|
||||
thread_message_id: if let ImageContext::ThreadMessage {
|
||||
thread_message_id: Some(id),
|
||||
} = context
|
||||
{
|
||||
Some(database::models::ThreadMessageId::from(id))
|
||||
} else {
|
||||
None
|
||||
},
|
||||
report_id: if let ImageContext::Report {
|
||||
report_id: Some(id),
|
||||
} = context
|
||||
{
|
||||
Some(database::models::ReportId::from(id))
|
||||
} else {
|
||||
None
|
||||
},
|
||||
};
|
||||
|
||||
// Insert
|
||||
db_image.insert(&mut transaction).await?;
|
||||
|
||||
let image = Image {
|
||||
id: db_image.id.into(),
|
||||
url: db_image.url,
|
||||
size: db_image.size,
|
||||
created: db_image.created,
|
||||
owner_id: db_image.owner_id.into(),
|
||||
context,
|
||||
};
|
||||
|
||||
transaction.commit().await?;
|
||||
|
||||
Ok(HttpResponse::Ok().json(image))
|
||||
}
|
||||
|
||||
@@ -42,6 +42,7 @@ use crate::{
|
||||
|
||||
use crate::database::models::oauth_client_item::OAuthClient as DBOAuthClient;
|
||||
use crate::models::ids::OAuthClientId as ApiOAuthClientId;
|
||||
use crate::util::img::{delete_old_images, upload_image_optimized};
|
||||
|
||||
pub fn config(cfg: &mut web::ServiceConfig) {
|
||||
cfg.service(
|
||||
@@ -135,12 +136,6 @@ pub struct NewOAuthApp {
|
||||
)]
|
||||
pub name: String,
|
||||
|
||||
#[validate(
|
||||
custom(function = "crate::util::validate::validate_url"),
|
||||
length(max = 255)
|
||||
)]
|
||||
pub icon_url: Option<String>,
|
||||
|
||||
#[validate(custom(function = "crate::util::validate::validate_no_restricted_scopes"))]
|
||||
pub max_scopes: Scopes,
|
||||
|
||||
@@ -190,7 +185,8 @@ pub async fn oauth_client_create<'a>(
|
||||
|
||||
let client = OAuthClient {
|
||||
id: client_id,
|
||||
icon_url: new_oauth_app.icon_url.clone(),
|
||||
icon_url: None,
|
||||
raw_icon_url: None,
|
||||
max_scopes: new_oauth_app.max_scopes,
|
||||
name: new_oauth_app.name.clone(),
|
||||
redirect_uris,
|
||||
@@ -349,63 +345,56 @@ pub async fn oauth_client_icon_edit(
|
||||
mut payload: web::Payload,
|
||||
session_queue: web::Data<AuthQueue>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
if let Some(content_type) = crate::util::ext::get_image_content_type(&ext.ext) {
|
||||
let cdn_url = dotenvy::var("CDN_URL")?;
|
||||
let user = get_user_from_headers(
|
||||
&req,
|
||||
&**pool,
|
||||
&redis,
|
||||
&session_queue,
|
||||
Some(&[Scopes::SESSION_ACCESS]),
|
||||
)
|
||||
let user = get_user_from_headers(
|
||||
&req,
|
||||
&**pool,
|
||||
&redis,
|
||||
&session_queue,
|
||||
Some(&[Scopes::SESSION_ACCESS]),
|
||||
)
|
||||
.await?
|
||||
.1;
|
||||
|
||||
let client = OAuthClient::get((*client_id).into(), &**pool)
|
||||
.await?
|
||||
.1;
|
||||
.ok_or_else(|| {
|
||||
ApiError::InvalidInput("The specified client does not exist!".to_string())
|
||||
})?;
|
||||
|
||||
let client = OAuthClient::get((*client_id).into(), &**pool)
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
ApiError::InvalidInput("The specified client does not exist!".to_string())
|
||||
})?;
|
||||
client.validate_authorized(Some(&user))?;
|
||||
|
||||
client.validate_authorized(Some(&user))?;
|
||||
delete_old_images(
|
||||
client.icon_url.clone(),
|
||||
client.raw_icon_url.clone(),
|
||||
&***file_host,
|
||||
)
|
||||
.await?;
|
||||
|
||||
if let Some(ref icon) = client.icon_url {
|
||||
let name = icon.split(&format!("{cdn_url}/")).nth(1);
|
||||
let bytes =
|
||||
read_from_payload(&mut payload, 262144, "Icons must be smaller than 256KiB").await?;
|
||||
let upload_result = upload_image_optimized(
|
||||
&format!("data/{}", client_id),
|
||||
bytes.freeze(),
|
||||
&ext.ext,
|
||||
Some(96),
|
||||
Some(1.0),
|
||||
&***file_host,
|
||||
)
|
||||
.await?;
|
||||
|
||||
if let Some(icon_path) = name {
|
||||
file_host.delete_file_version("", icon_path).await?;
|
||||
}
|
||||
}
|
||||
let mut transaction = pool.begin().await?;
|
||||
|
||||
let bytes =
|
||||
read_from_payload(&mut payload, 262144, "Icons must be smaller than 256KiB").await?;
|
||||
let hash = sha1::Sha1::from(&bytes).hexdigest();
|
||||
let upload_data = file_host
|
||||
.upload_file(
|
||||
content_type,
|
||||
&format!("data/{}/{}.{}", client_id, hash, ext.ext),
|
||||
bytes.freeze(),
|
||||
)
|
||||
.await?;
|
||||
let mut editable_client = client.clone();
|
||||
editable_client.icon_url = Some(upload_result.url);
|
||||
editable_client.raw_icon_url = Some(upload_result.raw_url);
|
||||
|
||||
let mut transaction = pool.begin().await?;
|
||||
editable_client
|
||||
.update_editable_fields(&mut *transaction)
|
||||
.await?;
|
||||
|
||||
let mut editable_client = client.clone();
|
||||
editable_client.icon_url = Some(format!("{}/{}", cdn_url, upload_data.file_name));
|
||||
transaction.commit().await?;
|
||||
|
||||
editable_client
|
||||
.update_editable_fields(&mut *transaction)
|
||||
.await?;
|
||||
|
||||
transaction.commit().await?;
|
||||
|
||||
Ok(HttpResponse::NoContent().body(""))
|
||||
} else {
|
||||
Err(ApiError::InvalidInput(format!(
|
||||
"Invalid format for project icon: {}",
|
||||
ext.ext
|
||||
)))
|
||||
}
|
||||
Ok(HttpResponse::NoContent().body(""))
|
||||
}
|
||||
|
||||
#[delete("app/{id}/icon")]
|
||||
@@ -417,7 +406,6 @@ pub async fn oauth_client_icon_delete(
|
||||
file_host: web::Data<Arc<dyn FileHost + Send + Sync>>,
|
||||
session_queue: web::Data<AuthQueue>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
let cdn_url = dotenvy::var("CDN_URL")?;
|
||||
let user = get_user_from_headers(
|
||||
&req,
|
||||
&**pool,
|
||||
@@ -435,18 +423,18 @@ pub async fn oauth_client_icon_delete(
|
||||
})?;
|
||||
client.validate_authorized(Some(&user))?;
|
||||
|
||||
if let Some(ref icon) = client.icon_url {
|
||||
let name = icon.split(&format!("{cdn_url}/")).nth(1);
|
||||
|
||||
if let Some(icon_path) = name {
|
||||
file_host.delete_file_version("", icon_path).await?;
|
||||
}
|
||||
}
|
||||
delete_old_images(
|
||||
client.icon_url.clone(),
|
||||
client.raw_icon_url.clone(),
|
||||
&***file_host,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let mut transaction = pool.begin().await?;
|
||||
|
||||
let mut editable_client = client.clone();
|
||||
editable_client.icon_url = None;
|
||||
editable_client.raw_icon_url = None;
|
||||
|
||||
editable_client
|
||||
.update_editable_fields(&mut *transaction)
|
||||
|
||||
@@ -14,6 +14,7 @@ use crate::models::pats::Scopes;
|
||||
use crate::models::teams::{OrganizationPermissions, ProjectPermissions};
|
||||
use crate::queue::session::AuthQueue;
|
||||
use crate::routes::v3::project_creation::CreateError;
|
||||
use crate::util::img::delete_old_images;
|
||||
use crate::util::routes::read_from_payload;
|
||||
use crate::util::validate::validation_errors_to_string;
|
||||
use crate::{database, models};
|
||||
@@ -164,6 +165,7 @@ pub async fn organization_create(
|
||||
description: new_organization.description.clone(),
|
||||
team_id,
|
||||
icon_url: None,
|
||||
raw_icon_url: None,
|
||||
color: None,
|
||||
};
|
||||
organization.clone().insert(&mut transaction).await?;
|
||||
@@ -926,98 +928,89 @@ pub async fn organization_icon_edit(
|
||||
mut payload: web::Payload,
|
||||
session_queue: web::Data<AuthQueue>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
if let Some(content_type) = crate::util::ext::get_image_content_type(&ext.ext) {
|
||||
let cdn_url = dotenvy::var("CDN_URL")?;
|
||||
let user = get_user_from_headers(
|
||||
&req,
|
||||
&**pool,
|
||||
&redis,
|
||||
&session_queue,
|
||||
Some(&[Scopes::ORGANIZATION_WRITE]),
|
||||
)
|
||||
let user = get_user_from_headers(
|
||||
&req,
|
||||
&**pool,
|
||||
&redis,
|
||||
&session_queue,
|
||||
Some(&[Scopes::ORGANIZATION_WRITE]),
|
||||
)
|
||||
.await?
|
||||
.1;
|
||||
let string = info.into_inner().0;
|
||||
|
||||
let organization_item = database::models::Organization::get(&string, &**pool, &redis)
|
||||
.await?
|
||||
.1;
|
||||
let string = info.into_inner().0;
|
||||
.ok_or_else(|| {
|
||||
ApiError::InvalidInput("The specified organization does not exist!".to_string())
|
||||
})?;
|
||||
|
||||
let organization_item = database::models::Organization::get(&string, &**pool, &redis)
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
ApiError::InvalidInput("The specified organization does not exist!".to_string())
|
||||
})?;
|
||||
|
||||
if !user.role.is_mod() {
|
||||
let team_member = database::models::TeamMember::get_from_user_id(
|
||||
organization_item.team_id,
|
||||
user.id.into(),
|
||||
&**pool,
|
||||
)
|
||||
.await
|
||||
.map_err(ApiError::Database)?;
|
||||
|
||||
let permissions =
|
||||
OrganizationPermissions::get_permissions_by_role(&user.role, &team_member)
|
||||
.unwrap_or_default();
|
||||
|
||||
if !permissions.contains(OrganizationPermissions::EDIT_DETAILS) {
|
||||
return Err(ApiError::CustomAuthentication(
|
||||
"You don't have permission to edit this organization's icon.".to_string(),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(icon) = organization_item.icon_url {
|
||||
let name = icon.split(&format!("{cdn_url}/")).nth(1);
|
||||
|
||||
if let Some(icon_path) = name {
|
||||
file_host.delete_file_version("", icon_path).await?;
|
||||
}
|
||||
}
|
||||
|
||||
let bytes =
|
||||
read_from_payload(&mut payload, 262144, "Icons must be smaller than 256KiB").await?;
|
||||
|
||||
let color = crate::util::img::get_color_from_img(&bytes)?;
|
||||
|
||||
let hash = sha1::Sha1::from(&bytes).hexdigest();
|
||||
let organization_id: OrganizationId = organization_item.id.into();
|
||||
let upload_data = file_host
|
||||
.upload_file(
|
||||
content_type,
|
||||
&format!("data/{}/{}.{}", organization_id, hash, ext.ext),
|
||||
bytes.freeze(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
let mut transaction = pool.begin().await?;
|
||||
|
||||
sqlx::query!(
|
||||
"
|
||||
UPDATE organizations
|
||||
SET icon_url = $1, color = $2
|
||||
WHERE (id = $3)
|
||||
",
|
||||
format!("{}/{}", cdn_url, upload_data.file_name),
|
||||
color.map(|x| x as i32),
|
||||
organization_item.id as database::models::ids::OrganizationId,
|
||||
if !user.role.is_mod() {
|
||||
let team_member = database::models::TeamMember::get_from_user_id(
|
||||
organization_item.team_id,
|
||||
user.id.into(),
|
||||
&**pool,
|
||||
)
|
||||
.execute(&mut *transaction)
|
||||
.await?;
|
||||
.await
|
||||
.map_err(ApiError::Database)?;
|
||||
|
||||
transaction.commit().await?;
|
||||
database::models::Organization::clear_cache(
|
||||
organization_item.id,
|
||||
Some(organization_item.slug),
|
||||
&redis,
|
||||
)
|
||||
.await?;
|
||||
let permissions =
|
||||
OrganizationPermissions::get_permissions_by_role(&user.role, &team_member)
|
||||
.unwrap_or_default();
|
||||
|
||||
Ok(HttpResponse::NoContent().body(""))
|
||||
} else {
|
||||
Err(ApiError::InvalidInput(format!(
|
||||
"Invalid format for project icon: {}",
|
||||
ext.ext
|
||||
)))
|
||||
if !permissions.contains(OrganizationPermissions::EDIT_DETAILS) {
|
||||
return Err(ApiError::CustomAuthentication(
|
||||
"You don't have permission to edit this organization's icon.".to_string(),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
delete_old_images(
|
||||
organization_item.icon_url,
|
||||
organization_item.raw_icon_url,
|
||||
&***file_host,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let bytes =
|
||||
read_from_payload(&mut payload, 262144, "Icons must be smaller than 256KiB").await?;
|
||||
|
||||
let organization_id: OrganizationId = organization_item.id.into();
|
||||
let upload_result = crate::util::img::upload_image_optimized(
|
||||
&format!("data/{}", organization_id),
|
||||
bytes.freeze(),
|
||||
&ext.ext,
|
||||
Some(96),
|
||||
Some(1.0),
|
||||
&***file_host,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let mut transaction = pool.begin().await?;
|
||||
|
||||
sqlx::query!(
|
||||
"
|
||||
UPDATE organizations
|
||||
SET icon_url = $1, raw_icon_url = $2, color = $3
|
||||
WHERE (id = $4)
|
||||
",
|
||||
upload_result.url,
|
||||
upload_result.raw_url,
|
||||
upload_result.color.map(|x| x as i32),
|
||||
organization_item.id as database::models::ids::OrganizationId,
|
||||
)
|
||||
.execute(&mut *transaction)
|
||||
.await?;
|
||||
|
||||
transaction.commit().await?;
|
||||
database::models::Organization::clear_cache(
|
||||
organization_item.id,
|
||||
Some(organization_item.slug),
|
||||
&redis,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(HttpResponse::NoContent().body(""))
|
||||
}
|
||||
|
||||
pub async fn delete_organization_icon(
|
||||
@@ -1065,21 +1058,19 @@ pub async fn delete_organization_icon(
|
||||
}
|
||||
}
|
||||
|
||||
let cdn_url = dotenvy::var("CDN_URL")?;
|
||||
if let Some(icon) = organization_item.icon_url {
|
||||
let name = icon.split(&format!("{cdn_url}/")).nth(1);
|
||||
|
||||
if let Some(icon_path) = name {
|
||||
file_host.delete_file_version("", icon_path).await?;
|
||||
}
|
||||
}
|
||||
delete_old_images(
|
||||
organization_item.icon_url,
|
||||
organization_item.raw_icon_url,
|
||||
&***file_host,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let mut transaction = pool.begin().await?;
|
||||
|
||||
sqlx::query!(
|
||||
"
|
||||
UPDATE organizations
|
||||
SET icon_url = NULL, color = NULL
|
||||
SET icon_url = NULL, raw_icon_url = NULL, color = NULL
|
||||
WHERE (id = $1)
|
||||
",
|
||||
organization_item.id as database::models::ids::OrganizationId,
|
||||
|
||||
@@ -6,6 +6,7 @@ use crate::database::models::{self, image_item, User};
|
||||
use crate::database::redis::RedisPool;
|
||||
use crate::file_hosting::{FileHost, FileHostingError};
|
||||
use crate::models::error::ApiError;
|
||||
use crate::models::ids::base62_impl::to_base62;
|
||||
use crate::models::ids::{ImageId, OrganizationId};
|
||||
use crate::models::images::{Image, ImageContext};
|
||||
use crate::models::pats::Scopes;
|
||||
@@ -17,6 +18,7 @@ use crate::models::threads::ThreadType;
|
||||
use crate::models::users::UserId;
|
||||
use crate::queue::session::AuthQueue;
|
||||
use crate::search::indexing::IndexingError;
|
||||
use crate::util::img::upload_image_optimized;
|
||||
use crate::util::routes::read_from_field;
|
||||
use crate::util::validate::validation_errors_to_string;
|
||||
use actix_multipart::{Field, Multipart};
|
||||
@@ -481,7 +483,6 @@ async fn project_create_inner(
|
||||
file_extension,
|
||||
file_host,
|
||||
field,
|
||||
&cdn_url,
|
||||
)
|
||||
.await?,
|
||||
);
|
||||
@@ -496,33 +497,40 @@ async fn project_create_inner(
|
||||
if let Some(item) = gallery_items.iter().find(|x| x.item == name) {
|
||||
let data = read_from_field(
|
||||
&mut field,
|
||||
5 * (1 << 20),
|
||||
"Gallery image exceeds the maximum of 5MiB.",
|
||||
2 * (1 << 20),
|
||||
"Gallery image exceeds the maximum of 2MiB.",
|
||||
)
|
||||
.await?;
|
||||
let hash = sha1::Sha1::from(&data).hexdigest();
|
||||
|
||||
let (_, file_extension) =
|
||||
super::version_creation::get_name_ext(&content_disposition)?;
|
||||
let content_type = crate::util::ext::get_image_content_type(file_extension)
|
||||
.ok_or_else(|| {
|
||||
CreateError::InvalidIconFormat(file_extension.to_string())
|
||||
})?;
|
||||
let url = format!("data/{project_id}/images/{hash}.{file_extension}");
|
||||
let upload_data = file_host
|
||||
.upload_file(content_type, &url, data.freeze())
|
||||
.await?;
|
||||
|
||||
let url = format!("data/{project_id}/images");
|
||||
let upload_result = upload_image_optimized(
|
||||
&url,
|
||||
data.freeze(),
|
||||
file_extension,
|
||||
Some(350),
|
||||
Some(1.0),
|
||||
file_host,
|
||||
)
|
||||
.await
|
||||
.map_err(|e| CreateError::InvalidIconFormat(e.to_string()))?;
|
||||
|
||||
uploaded_files.push(UploadedFile {
|
||||
file_id: upload_data.file_id,
|
||||
file_name: upload_data.file_name,
|
||||
file_id: upload_result.raw_url_path.clone(),
|
||||
file_name: upload_result.raw_url_path,
|
||||
});
|
||||
gallery_urls.push(crate::models::projects::GalleryItem {
|
||||
url: format!("{cdn_url}/{url}"),
|
||||
url: upload_result.url,
|
||||
raw_url: upload_result.raw_url,
|
||||
featured: item.featured,
|
||||
name: item.name.clone(),
|
||||
description: item.description.clone(),
|
||||
created: Utc::now(),
|
||||
ordering: item.ordering,
|
||||
});
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
@@ -715,6 +723,7 @@ async fn project_create_inner(
|
||||
summary: project_create_data.summary,
|
||||
description: project_create_data.description,
|
||||
icon_url: icon_data.clone().map(|x| x.0),
|
||||
raw_icon_url: icon_data.clone().map(|x| x.1),
|
||||
|
||||
license_url: project_create_data.license_url,
|
||||
categories,
|
||||
@@ -729,6 +738,7 @@ async fn project_create_inner(
|
||||
.iter()
|
||||
.map(|x| models::project_item::GalleryItem {
|
||||
image_url: x.url.clone(),
|
||||
raw_image_url: x.raw_url.clone(),
|
||||
featured: x.featured,
|
||||
name: x.name.clone(),
|
||||
description: x.description.clone(),
|
||||
@@ -736,7 +746,7 @@ async fn project_create_inner(
|
||||
ordering: x.ordering,
|
||||
})
|
||||
.collect(),
|
||||
color: icon_data.and_then(|x| x.1),
|
||||
color: icon_data.and_then(|x| x.2),
|
||||
monetization_status: MonetizationStatus::Monetized,
|
||||
};
|
||||
let project_builder = project_builder_actual.clone();
|
||||
@@ -943,29 +953,32 @@ async fn process_icon_upload(
|
||||
file_extension: &str,
|
||||
file_host: &dyn FileHost,
|
||||
mut field: Field,
|
||||
cdn_url: &str,
|
||||
) -> Result<(String, Option<u32>), CreateError> {
|
||||
if let Some(content_type) = crate::util::ext::get_image_content_type(file_extension) {
|
||||
let data = read_from_field(&mut field, 262144, "Icons must be smaller than 256KiB").await?;
|
||||
) -> Result<(String, String, Option<u32>), CreateError> {
|
||||
let data = read_from_field(&mut field, 262144, "Icons must be smaller than 256KiB").await?;
|
||||
let upload_result = crate::util::img::upload_image_optimized(
|
||||
&format!("data/{}", to_base62(id)),
|
||||
data.freeze(),
|
||||
file_extension,
|
||||
Some(96),
|
||||
Some(1.0),
|
||||
file_host,
|
||||
)
|
||||
.await
|
||||
.map_err(|e| CreateError::InvalidIconFormat(e.to_string()))?;
|
||||
|
||||
let color = crate::util::img::get_color_from_img(&data)?;
|
||||
uploaded_files.push(UploadedFile {
|
||||
file_id: upload_result.raw_url_path.clone(),
|
||||
file_name: upload_result.raw_url_path,
|
||||
});
|
||||
|
||||
let hash = sha1::Sha1::from(&data).hexdigest();
|
||||
let upload_data = file_host
|
||||
.upload_file(
|
||||
content_type,
|
||||
&format!("data/{id}/{hash}.{file_extension}"),
|
||||
data.freeze(),
|
||||
)
|
||||
.await?;
|
||||
uploaded_files.push(UploadedFile {
|
||||
file_id: upload_result.url_path.clone(),
|
||||
file_name: upload_result.url_path,
|
||||
});
|
||||
|
||||
uploaded_files.push(UploadedFile {
|
||||
file_id: upload_data.file_id,
|
||||
file_name: upload_data.file_name.clone(),
|
||||
});
|
||||
|
||||
Ok((format!("{}/{}", cdn_url, upload_data.file_name), color))
|
||||
} else {
|
||||
Err(CreateError::InvalidIconFormat(file_extension.to_string()))
|
||||
}
|
||||
Ok((
|
||||
upload_result.url,
|
||||
upload_result.raw_url,
|
||||
upload_result.color,
|
||||
))
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ use crate::routes::ApiError;
|
||||
use crate::search::indexing::remove_documents;
|
||||
use crate::search::{search_for_project, SearchConfig, SearchError};
|
||||
use crate::util::img;
|
||||
use crate::util::img::{delete_old_images, upload_image_optimized};
|
||||
use crate::util::routes::read_from_payload;
|
||||
use crate::util::validate::validation_errors_to_string;
|
||||
use actix_web::{web, HttpRequest, HttpResponse};
|
||||
@@ -1317,109 +1318,95 @@ pub async fn project_icon_edit(
|
||||
mut payload: web::Payload,
|
||||
session_queue: web::Data<AuthQueue>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
if let Some(content_type) = crate::util::ext::get_image_content_type(&ext.ext) {
|
||||
let cdn_url = dotenvy::var("CDN_URL")?;
|
||||
let user = get_user_from_headers(
|
||||
&req,
|
||||
&**pool,
|
||||
&redis,
|
||||
&session_queue,
|
||||
Some(&[Scopes::PROJECT_WRITE]),
|
||||
)
|
||||
let user = get_user_from_headers(
|
||||
&req,
|
||||
&**pool,
|
||||
&redis,
|
||||
&session_queue,
|
||||
Some(&[Scopes::PROJECT_WRITE]),
|
||||
)
|
||||
.await?
|
||||
.1;
|
||||
let string = info.into_inner().0;
|
||||
|
||||
let project_item = db_models::Project::get(&string, &**pool, &redis)
|
||||
.await?
|
||||
.1;
|
||||
let string = info.into_inner().0;
|
||||
.ok_or_else(|| {
|
||||
ApiError::InvalidInput("The specified project does not exist!".to_string())
|
||||
})?;
|
||||
|
||||
let project_item = db_models::Project::get(&string, &**pool, &redis)
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
ApiError::InvalidInput("The specified project does not exist!".to_string())
|
||||
})?;
|
||||
|
||||
if !user.role.is_mod() {
|
||||
let (team_member, organization_team_member) =
|
||||
db_models::TeamMember::get_for_project_permissions(
|
||||
&project_item.inner,
|
||||
user.id.into(),
|
||||
&**pool,
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Hide the project
|
||||
if team_member.is_none() && organization_team_member.is_none() {
|
||||
return Err(ApiError::CustomAuthentication(
|
||||
"The specified project does not exist!".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
let permissions = ProjectPermissions::get_permissions_by_role(
|
||||
&user.role,
|
||||
&team_member,
|
||||
&organization_team_member,
|
||||
)
|
||||
.unwrap_or_default();
|
||||
|
||||
if !permissions.contains(ProjectPermissions::EDIT_DETAILS) {
|
||||
return Err(ApiError::CustomAuthentication(
|
||||
"You don't have permission to edit this project's icon.".to_string(),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(icon) = project_item.inner.icon_url {
|
||||
let name = icon.split(&format!("{cdn_url}/")).nth(1);
|
||||
|
||||
if let Some(icon_path) = name {
|
||||
file_host.delete_file_version("", icon_path).await?;
|
||||
}
|
||||
}
|
||||
|
||||
let bytes =
|
||||
read_from_payload(&mut payload, 262144, "Icons must be smaller than 256KiB").await?;
|
||||
|
||||
let color = crate::util::img::get_color_from_img(&bytes)?;
|
||||
|
||||
let hash = sha1::Sha1::from(&bytes).hexdigest();
|
||||
let project_id: ProjectId = project_item.inner.id.into();
|
||||
let upload_data = file_host
|
||||
.upload_file(
|
||||
content_type,
|
||||
&format!("data/{}/{}.{}", project_id, hash, ext.ext),
|
||||
bytes.freeze(),
|
||||
if !user.role.is_mod() {
|
||||
let (team_member, organization_team_member) =
|
||||
db_models::TeamMember::get_for_project_permissions(
|
||||
&project_item.inner,
|
||||
user.id.into(),
|
||||
&**pool,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let mut transaction = pool.begin().await?;
|
||||
// Hide the project
|
||||
if team_member.is_none() && organization_team_member.is_none() {
|
||||
return Err(ApiError::CustomAuthentication(
|
||||
"The specified project does not exist!".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
sqlx::query!(
|
||||
"
|
||||
UPDATE mods
|
||||
SET icon_url = $1, color = $2
|
||||
WHERE (id = $3)
|
||||
",
|
||||
format!("{}/{}", cdn_url, upload_data.file_name),
|
||||
color.map(|x| x as i32),
|
||||
project_item.inner.id as db_ids::ProjectId,
|
||||
let permissions = ProjectPermissions::get_permissions_by_role(
|
||||
&user.role,
|
||||
&team_member,
|
||||
&organization_team_member,
|
||||
)
|
||||
.execute(&mut *transaction)
|
||||
.await?;
|
||||
.unwrap_or_default();
|
||||
|
||||
transaction.commit().await?;
|
||||
db_models::Project::clear_cache(
|
||||
project_item.inner.id,
|
||||
project_item.inner.slug,
|
||||
None,
|
||||
&redis,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(HttpResponse::NoContent().body(""))
|
||||
} else {
|
||||
Err(ApiError::InvalidInput(format!(
|
||||
"Invalid format for project icon: {}",
|
||||
ext.ext
|
||||
)))
|
||||
if !permissions.contains(ProjectPermissions::EDIT_DETAILS) {
|
||||
return Err(ApiError::CustomAuthentication(
|
||||
"You don't have permission to edit this project's icon.".to_string(),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
delete_old_images(
|
||||
project_item.inner.icon_url,
|
||||
project_item.inner.raw_icon_url,
|
||||
&***file_host,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let bytes =
|
||||
read_from_payload(&mut payload, 262144, "Icons must be smaller than 256KiB").await?;
|
||||
|
||||
let project_id: ProjectId = project_item.inner.id.into();
|
||||
let upload_result = upload_image_optimized(
|
||||
&format!("data/{}", project_id),
|
||||
bytes.freeze(),
|
||||
&ext.ext,
|
||||
Some(96),
|
||||
Some(1.0),
|
||||
&***file_host,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let mut transaction = pool.begin().await?;
|
||||
|
||||
sqlx::query!(
|
||||
"
|
||||
UPDATE mods
|
||||
SET icon_url = $1, raw_icon_url = $2, color = $3
|
||||
WHERE (id = $4)
|
||||
",
|
||||
upload_result.url,
|
||||
upload_result.raw_url,
|
||||
upload_result.color.map(|x| x as i32),
|
||||
project_item.inner.id as db_ids::ProjectId,
|
||||
)
|
||||
.execute(&mut *transaction)
|
||||
.await?;
|
||||
|
||||
transaction.commit().await?;
|
||||
db_models::Project::clear_cache(project_item.inner.id, project_item.inner.slug, None, &redis)
|
||||
.await?;
|
||||
|
||||
Ok(HttpResponse::NoContent().body(""))
|
||||
}
|
||||
|
||||
pub async fn delete_project_icon(
|
||||
@@ -1476,21 +1463,19 @@ pub async fn delete_project_icon(
|
||||
}
|
||||
}
|
||||
|
||||
let cdn_url = dotenvy::var("CDN_URL")?;
|
||||
if let Some(icon) = project_item.inner.icon_url {
|
||||
let name = icon.split(&format!("{cdn_url}/")).nth(1);
|
||||
|
||||
if let Some(icon_path) = name {
|
||||
file_host.delete_file_version("", icon_path).await?;
|
||||
}
|
||||
}
|
||||
delete_old_images(
|
||||
project_item.inner.icon_url,
|
||||
project_item.inner.raw_icon_url,
|
||||
&***file_host,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let mut transaction = pool.begin().await?;
|
||||
|
||||
sqlx::query!(
|
||||
"
|
||||
UPDATE mods
|
||||
SET icon_url = NULL, color = NULL
|
||||
SET icon_url = NULL, raw_icon_url = NULL, color = NULL
|
||||
WHERE (id = $1)
|
||||
",
|
||||
project_item.inner.id as db_ids::ProjectId,
|
||||
@@ -1527,132 +1512,122 @@ pub async fn add_gallery_item(
|
||||
mut payload: web::Payload,
|
||||
session_queue: web::Data<AuthQueue>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
if let Some(content_type) = crate::util::ext::get_image_content_type(&ext.ext) {
|
||||
item.validate()
|
||||
.map_err(|err| ApiError::Validation(validation_errors_to_string(err, None)))?;
|
||||
item.validate()
|
||||
.map_err(|err| ApiError::Validation(validation_errors_to_string(err, None)))?;
|
||||
|
||||
let cdn_url = dotenvy::var("CDN_URL")?;
|
||||
let user = get_user_from_headers(
|
||||
&req,
|
||||
&**pool,
|
||||
&redis,
|
||||
&session_queue,
|
||||
Some(&[Scopes::PROJECT_WRITE]),
|
||||
)
|
||||
let user = get_user_from_headers(
|
||||
&req,
|
||||
&**pool,
|
||||
&redis,
|
||||
&session_queue,
|
||||
Some(&[Scopes::PROJECT_WRITE]),
|
||||
)
|
||||
.await?
|
||||
.1;
|
||||
let string = info.into_inner().0;
|
||||
|
||||
let project_item = db_models::Project::get(&string, &**pool, &redis)
|
||||
.await?
|
||||
.1;
|
||||
let string = info.into_inner().0;
|
||||
.ok_or_else(|| {
|
||||
ApiError::InvalidInput("The specified project does not exist!".to_string())
|
||||
})?;
|
||||
|
||||
let project_item = db_models::Project::get(&string, &**pool, &redis)
|
||||
.await?
|
||||
.ok_or_else(|| {
|
||||
ApiError::InvalidInput("The specified project does not exist!".to_string())
|
||||
})?;
|
||||
if project_item.gallery_items.len() > 64 {
|
||||
return Err(ApiError::CustomAuthentication(
|
||||
"You have reached the maximum of gallery images to upload.".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
if project_item.gallery_items.len() > 64 {
|
||||
return Err(ApiError::CustomAuthentication(
|
||||
"You have reached the maximum of gallery images to upload.".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
if !user.role.is_admin() {
|
||||
let (team_member, organization_team_member) =
|
||||
db_models::TeamMember::get_for_project_permissions(
|
||||
&project_item.inner,
|
||||
user.id.into(),
|
||||
&**pool,
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Hide the project
|
||||
if team_member.is_none() && organization_team_member.is_none() {
|
||||
return Err(ApiError::CustomAuthentication(
|
||||
"The specified project does not exist!".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
let permissions = ProjectPermissions::get_permissions_by_role(
|
||||
&user.role,
|
||||
&team_member,
|
||||
&organization_team_member,
|
||||
if !user.role.is_admin() {
|
||||
let (team_member, organization_team_member) =
|
||||
db_models::TeamMember::get_for_project_permissions(
|
||||
&project_item.inner,
|
||||
user.id.into(),
|
||||
&**pool,
|
||||
)
|
||||
.unwrap_or_default();
|
||||
|
||||
if !permissions.contains(ProjectPermissions::EDIT_DETAILS) {
|
||||
return Err(ApiError::CustomAuthentication(
|
||||
"You don't have permission to edit this project's gallery.".to_string(),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
let bytes = read_from_payload(
|
||||
&mut payload,
|
||||
5 * (1 << 20),
|
||||
"Gallery image exceeds the maximum of 5MiB.",
|
||||
)
|
||||
.await?;
|
||||
let hash = sha1::Sha1::from(&bytes).hexdigest();
|
||||
|
||||
let id: ProjectId = project_item.inner.id.into();
|
||||
let url = format!("data/{}/images/{}.{}", id, hash, &*ext.ext);
|
||||
|
||||
let file_url = format!("{cdn_url}/{url}");
|
||||
if project_item
|
||||
.gallery_items
|
||||
.iter()
|
||||
.any(|x| x.image_url == file_url)
|
||||
{
|
||||
return Err(ApiError::InvalidInput(
|
||||
"You may not upload duplicate gallery images!".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
file_host
|
||||
.upload_file(content_type, &url, bytes.freeze())
|
||||
.await?;
|
||||
|
||||
let mut transaction = pool.begin().await?;
|
||||
// Hide the project
|
||||
if team_member.is_none() && organization_team_member.is_none() {
|
||||
return Err(ApiError::CustomAuthentication(
|
||||
"The specified project does not exist!".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
if item.featured {
|
||||
sqlx::query!(
|
||||
"
|
||||
let permissions = ProjectPermissions::get_permissions_by_role(
|
||||
&user.role,
|
||||
&team_member,
|
||||
&organization_team_member,
|
||||
)
|
||||
.unwrap_or_default();
|
||||
|
||||
if !permissions.contains(ProjectPermissions::EDIT_DETAILS) {
|
||||
return Err(ApiError::CustomAuthentication(
|
||||
"You don't have permission to edit this project's gallery.".to_string(),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
let bytes = read_from_payload(
|
||||
&mut payload,
|
||||
2 * (1 << 20),
|
||||
"Gallery image exceeds the maximum of 2MiB.",
|
||||
)
|
||||
.await?;
|
||||
|
||||
let id: ProjectId = project_item.inner.id.into();
|
||||
let upload_result = upload_image_optimized(
|
||||
&format!("data/{}/images", id),
|
||||
bytes.freeze(),
|
||||
&ext.ext,
|
||||
Some(350),
|
||||
Some(1.0),
|
||||
&***file_host,
|
||||
)
|
||||
.await?;
|
||||
|
||||
if project_item
|
||||
.gallery_items
|
||||
.iter()
|
||||
.any(|x| x.image_url == upload_result.url)
|
||||
{
|
||||
return Err(ApiError::InvalidInput(
|
||||
"You may not upload duplicate gallery images!".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
let mut transaction = pool.begin().await?;
|
||||
|
||||
if item.featured {
|
||||
sqlx::query!(
|
||||
"
|
||||
UPDATE mods_gallery
|
||||
SET featured = $2
|
||||
WHERE mod_id = $1
|
||||
",
|
||||
project_item.inner.id as db_ids::ProjectId,
|
||||
false,
|
||||
)
|
||||
.execute(&mut *transaction)
|
||||
.await?;
|
||||
}
|
||||
|
||||
let gallery_item = vec![db_models::project_item::GalleryItem {
|
||||
image_url: file_url,
|
||||
featured: item.featured,
|
||||
name: item.name,
|
||||
description: item.description,
|
||||
created: Utc::now(),
|
||||
ordering: item.ordering.unwrap_or(0),
|
||||
}];
|
||||
GalleryItem::insert_many(gallery_item, project_item.inner.id, &mut transaction).await?;
|
||||
|
||||
transaction.commit().await?;
|
||||
db_models::Project::clear_cache(
|
||||
project_item.inner.id,
|
||||
project_item.inner.slug,
|
||||
None,
|
||||
&redis,
|
||||
project_item.inner.id as db_ids::ProjectId,
|
||||
false,
|
||||
)
|
||||
.execute(&mut *transaction)
|
||||
.await?;
|
||||
}
|
||||
|
||||
let gallery_item = vec![db_models::project_item::GalleryItem {
|
||||
image_url: upload_result.url,
|
||||
raw_image_url: upload_result.raw_url,
|
||||
featured: item.featured,
|
||||
name: item.name,
|
||||
description: item.description,
|
||||
created: Utc::now(),
|
||||
ordering: item.ordering.unwrap_or(0),
|
||||
}];
|
||||
GalleryItem::insert_many(gallery_item, project_item.inner.id, &mut transaction).await?;
|
||||
|
||||
transaction.commit().await?;
|
||||
db_models::Project::clear_cache(project_item.inner.id, project_item.inner.slug, None, &redis)
|
||||
.await?;
|
||||
|
||||
Ok(HttpResponse::NoContent().body(""))
|
||||
} else {
|
||||
Err(ApiError::InvalidInput(format!(
|
||||
"Invalid format for gallery image: {}",
|
||||
ext.ext
|
||||
)))
|
||||
}
|
||||
Ok(HttpResponse::NoContent().body(""))
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Validate)]
|
||||
@@ -1891,9 +1866,9 @@ pub async fn delete_gallery_item(
|
||||
}
|
||||
let mut transaction = pool.begin().await?;
|
||||
|
||||
let id = sqlx::query!(
|
||||
let item = sqlx::query!(
|
||||
"
|
||||
SELECT id FROM mods_gallery
|
||||
SELECT id, image_url, raw_image_url FROM mods_gallery
|
||||
WHERE image_url = $1
|
||||
",
|
||||
item.url
|
||||
@@ -1905,15 +1880,14 @@ pub async fn delete_gallery_item(
|
||||
"Gallery item at URL {} is not part of the project's gallery.",
|
||||
item.url
|
||||
))
|
||||
})?
|
||||
.id;
|
||||
})?;
|
||||
|
||||
let cdn_url = dotenvy::var("CDN_URL")?;
|
||||
let name = item.url.split(&format!("{cdn_url}/")).nth(1);
|
||||
|
||||
if let Some(icon_path) = name {
|
||||
file_host.delete_file_version("", icon_path).await?;
|
||||
}
|
||||
delete_old_images(
|
||||
Some(item.image_url),
|
||||
Some(item.raw_image_url),
|
||||
&***file_host,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let mut transaction = pool.begin().await?;
|
||||
|
||||
@@ -1922,7 +1896,7 @@ pub async fn delete_gallery_item(
|
||||
DELETE FROM mods_gallery
|
||||
WHERE id = $1
|
||||
",
|
||||
id
|
||||
item.id
|
||||
)
|
||||
.execute(&mut *transaction)
|
||||
.await?;
|
||||
|
||||
@@ -7,6 +7,8 @@ use serde::{Deserialize, Serialize};
|
||||
use sqlx::PgPool;
|
||||
use validator::Validate;
|
||||
|
||||
use super::{oauth_clients::get_user_clients, ApiError};
|
||||
use crate::util::img::delete_old_images;
|
||||
use crate::{
|
||||
auth::{filter_visible_projects, get_user_from_headers},
|
||||
database::{models::User, redis::RedisPool},
|
||||
@@ -23,8 +25,6 @@ use crate::{
|
||||
util::{routes::read_from_payload, validate::validation_errors_to_string},
|
||||
};
|
||||
|
||||
use super::{oauth_clients::get_user_clients, ApiError};
|
||||
|
||||
pub fn config(cfg: &mut web::ServiceConfig) {
|
||||
cfg.route("user", web::get().to(user_auth_get));
|
||||
cfg.route("users", web::get().to(users_get));
|
||||
@@ -446,71 +446,62 @@ pub async fn user_icon_edit(
|
||||
mut payload: web::Payload,
|
||||
session_queue: web::Data<AuthQueue>,
|
||||
) -> Result<HttpResponse, ApiError> {
|
||||
if let Some(content_type) = crate::util::ext::get_image_content_type(&ext.ext) {
|
||||
let cdn_url = dotenvy::var("CDN_URL")?;
|
||||
let user = get_user_from_headers(
|
||||
&req,
|
||||
&**pool,
|
||||
&redis,
|
||||
&session_queue,
|
||||
Some(&[Scopes::USER_WRITE]),
|
||||
)
|
||||
.await?
|
||||
.1;
|
||||
let id_option = User::get(&info.into_inner().0, &**pool, &redis).await?;
|
||||
let user = get_user_from_headers(
|
||||
&req,
|
||||
&**pool,
|
||||
&redis,
|
||||
&session_queue,
|
||||
Some(&[Scopes::USER_WRITE]),
|
||||
)
|
||||
.await?
|
||||
.1;
|
||||
let id_option = User::get(&info.into_inner().0, &**pool, &redis).await?;
|
||||
|
||||
if let Some(actual_user) = id_option {
|
||||
if user.id != actual_user.id.into() && !user.role.is_mod() {
|
||||
return Err(ApiError::CustomAuthentication(
|
||||
"You don't have permission to edit this user's icon.".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
let icon_url = actual_user.avatar_url;
|
||||
let user_id: UserId = actual_user.id.into();
|
||||
|
||||
if let Some(icon) = icon_url {
|
||||
let name = icon.split(&format!("{cdn_url}/")).nth(1);
|
||||
|
||||
if let Some(icon_path) = name {
|
||||
file_host.delete_file_version("", icon_path).await?;
|
||||
}
|
||||
}
|
||||
|
||||
let bytes =
|
||||
read_from_payload(&mut payload, 2097152, "Icons must be smaller than 2MiB").await?;
|
||||
|
||||
let hash = sha1::Sha1::from(&bytes).hexdigest();
|
||||
let upload_data = file_host
|
||||
.upload_file(
|
||||
content_type,
|
||||
&format!("user/{}/{}.{}", user_id, hash, ext.ext),
|
||||
bytes.freeze(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
sqlx::query!(
|
||||
"
|
||||
UPDATE users
|
||||
SET avatar_url = $1
|
||||
WHERE (id = $2)
|
||||
",
|
||||
format!("{}/{}", cdn_url, upload_data.file_name),
|
||||
actual_user.id as crate::database::models::ids::UserId,
|
||||
)
|
||||
.execute(&**pool)
|
||||
.await?;
|
||||
User::clear_caches(&[(actual_user.id, None)], &redis).await?;
|
||||
|
||||
Ok(HttpResponse::NoContent().body(""))
|
||||
} else {
|
||||
Err(ApiError::NotFound)
|
||||
if let Some(actual_user) = id_option {
|
||||
if user.id != actual_user.id.into() && !user.role.is_mod() {
|
||||
return Err(ApiError::CustomAuthentication(
|
||||
"You don't have permission to edit this user's icon.".to_string(),
|
||||
));
|
||||
}
|
||||
|
||||
delete_old_images(
|
||||
actual_user.avatar_url,
|
||||
actual_user.raw_avatar_url,
|
||||
&***file_host,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let bytes =
|
||||
read_from_payload(&mut payload, 262144, "Icons must be smaller than 256KiB").await?;
|
||||
|
||||
let user_id: UserId = actual_user.id.into();
|
||||
let upload_result = crate::util::img::upload_image_optimized(
|
||||
&format!("data/{}", user_id),
|
||||
bytes.freeze(),
|
||||
&ext.ext,
|
||||
Some(96),
|
||||
Some(1.0),
|
||||
&***file_host,
|
||||
)
|
||||
.await?;
|
||||
|
||||
sqlx::query!(
|
||||
"
|
||||
UPDATE users
|
||||
SET avatar_url = $1, raw_avatar_url = $2
|
||||
WHERE (id = $3)
|
||||
",
|
||||
upload_result.url,
|
||||
upload_result.raw_url,
|
||||
actual_user.id as crate::database::models::ids::UserId,
|
||||
)
|
||||
.execute(&**pool)
|
||||
.await?;
|
||||
User::clear_caches(&[(actual_user.id, None)], &redis).await?;
|
||||
|
||||
Ok(HttpResponse::NoContent().body(""))
|
||||
} else {
|
||||
Err(ApiError::InvalidInput(format!(
|
||||
"Invalid format for user icon: {}",
|
||||
ext.ext
|
||||
)))
|
||||
Err(ApiError::NotFound)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
155
src/util/img.rs
155
src/util/img.rs
@@ -1,11 +1,14 @@
|
||||
use crate::database;
|
||||
use crate::database::models::image_item;
|
||||
use crate::database::redis::RedisPool;
|
||||
use crate::file_hosting::FileHost;
|
||||
use crate::models::images::ImageContext;
|
||||
use crate::routes::ApiError;
|
||||
use color_thief::ColorFormat;
|
||||
use image::imageops::FilterType;
|
||||
use image::{EncodableLayout, ImageError};
|
||||
use image::{DynamicImage, EncodableLayout, GenericImageView, ImageError, ImageOutputFormat};
|
||||
use std::io::Cursor;
|
||||
use webp::Encoder;
|
||||
|
||||
pub fn get_color_from_img(data: &[u8]) -> Result<Option<u32>, ImageError> {
|
||||
let image = image::load_from_memory(data)?
|
||||
@@ -19,6 +22,156 @@ pub fn get_color_from_img(data: &[u8]) -> Result<Option<u32>, ImageError> {
|
||||
Ok(color)
|
||||
}
|
||||
|
||||
pub struct UploadImageResult {
|
||||
pub url: String,
|
||||
pub url_path: String,
|
||||
|
||||
pub raw_url: String,
|
||||
pub raw_url_path: String,
|
||||
|
||||
pub color: Option<u32>,
|
||||
}
|
||||
|
||||
pub async fn upload_image_optimized(
|
||||
upload_folder: &str,
|
||||
bytes: bytes::Bytes,
|
||||
file_extension: &str,
|
||||
target_width: Option<u32>,
|
||||
min_aspect_ratio: Option<f32>,
|
||||
file_host: &dyn FileHost,
|
||||
) -> Result<UploadImageResult, ApiError> {
|
||||
let content_type =
|
||||
crate::util::ext::get_image_content_type(file_extension).ok_or_else(|| {
|
||||
ApiError::InvalidInput(format!("Invalid format for image: {}", file_extension))
|
||||
})?;
|
||||
|
||||
let cdn_url = dotenvy::var("CDN_URL")?;
|
||||
|
||||
let hash = sha1::Sha1::from(&bytes).hexdigest();
|
||||
let (processed_image, processed_image_ext) =
|
||||
process_image(bytes.clone(), content_type, target_width, min_aspect_ratio)?;
|
||||
let color = get_color_from_img(&bytes)?;
|
||||
|
||||
// Only upload the processed image if it's smaller than the original
|
||||
let processed_upload_data = if processed_image.len() < bytes.len() {
|
||||
Some(
|
||||
file_host
|
||||
.upload_file(
|
||||
content_type,
|
||||
&format!(
|
||||
"{}/{}_{}.{}",
|
||||
upload_folder,
|
||||
hash,
|
||||
target_width.unwrap_or(0),
|
||||
processed_image_ext
|
||||
),
|
||||
processed_image,
|
||||
)
|
||||
.await?,
|
||||
)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let upload_data = file_host
|
||||
.upload_file(
|
||||
content_type,
|
||||
&format!("{}/{}.{}", upload_folder, hash, file_extension),
|
||||
bytes,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let url = format!("{}/{}", cdn_url, upload_data.file_name);
|
||||
Ok(UploadImageResult {
|
||||
url: processed_upload_data
|
||||
.clone()
|
||||
.map(|x| format!("{}/{}", cdn_url, x.file_name))
|
||||
.unwrap_or_else(|| url.clone()),
|
||||
url_path: processed_upload_data
|
||||
.map(|x| x.file_name)
|
||||
.unwrap_or_else(|| upload_data.file_name.clone()),
|
||||
|
||||
raw_url: url,
|
||||
raw_url_path: upload_data.file_name,
|
||||
color,
|
||||
})
|
||||
}
|
||||
|
||||
fn process_image(
|
||||
image_bytes: bytes::Bytes,
|
||||
content_type: &str,
|
||||
target_width: Option<u32>,
|
||||
min_aspect_ratio: Option<f32>,
|
||||
) -> Result<(bytes::Bytes, String), ImageError> {
|
||||
if content_type.to_lowercase() == "image/gif" {
|
||||
return Ok((image_bytes.clone(), "gif".to_string()));
|
||||
}
|
||||
|
||||
let mut img = image::load_from_memory(&image_bytes)?;
|
||||
|
||||
let webp_bytes = convert_to_webp(&img)?;
|
||||
img = image::load_from_memory(&webp_bytes)?;
|
||||
|
||||
// Resize the image
|
||||
let (orig_width, orig_height) = img.dimensions();
|
||||
let aspect_ratio = orig_width as f32 / orig_height as f32;
|
||||
|
||||
if let Some(target_width) = target_width {
|
||||
if img.width() > target_width {
|
||||
let new_height = (target_width as f32 / aspect_ratio).round() as u32;
|
||||
img = img.resize(target_width, new_height, FilterType::Lanczos3);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(min_aspect_ratio) = min_aspect_ratio {
|
||||
// Crop if necessary
|
||||
if aspect_ratio < min_aspect_ratio {
|
||||
let crop_height = (img.width() as f32 / min_aspect_ratio).round() as u32;
|
||||
let y_offset = (img.height() - crop_height) / 2;
|
||||
img = img.crop_imm(0, y_offset, img.width(), crop_height);
|
||||
}
|
||||
}
|
||||
|
||||
// Optimize and compress
|
||||
let mut output = Vec::new();
|
||||
img.write_to(&mut Cursor::new(&mut output), ImageOutputFormat::WebP)?;
|
||||
|
||||
Ok((bytes::Bytes::from(output), "webp".to_string()))
|
||||
}
|
||||
|
||||
fn convert_to_webp(img: &DynamicImage) -> Result<Vec<u8>, ImageError> {
|
||||
let rgba = img.to_rgba8();
|
||||
let encoder = Encoder::from_rgba(&rgba, img.width(), img.height());
|
||||
let webp = encoder.encode(75.0); // Quality factor: 0-100, 75 is a good balance
|
||||
Ok(webp.to_vec())
|
||||
}
|
||||
|
||||
pub async fn delete_old_images(
|
||||
image_url: Option<String>,
|
||||
raw_image_url: Option<String>,
|
||||
file_host: &dyn FileHost,
|
||||
) -> Result<(), ApiError> {
|
||||
let cdn_url = dotenvy::var("CDN_URL")?;
|
||||
let cdn_url_start = format!("{cdn_url}/");
|
||||
if let Some(image_url) = image_url {
|
||||
let name = image_url.split(&cdn_url_start).nth(1);
|
||||
|
||||
if let Some(icon_path) = name {
|
||||
file_host.delete_file_version("", icon_path).await?;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(raw_image_url) = raw_image_url {
|
||||
let name = raw_image_url.split(&cdn_url_start).nth(1);
|
||||
|
||||
if let Some(icon_path) = name {
|
||||
file_host.delete_file_version("", icon_path).await?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// check changes to associated images
|
||||
// if they no longer exist in the String list, delete them
|
||||
// Eg: if description is modified and no longer contains a link to an iamge
|
||||
|
||||
Reference in New Issue
Block a user