mirror of
https://github.com/captbaritone/webamp.git
synced 2026-07-23 10:07:35 +00:00
Add query param to use the graphql endpoint
This commit is contained in:
parent
1bf510ec0d
commit
4af0a37011
6 changed files with 358 additions and 110 deletions
|
|
@ -6,6 +6,25 @@ import { useActionCreator } from "./hooks";
|
|||
|
||||
import { useCallback } from "react";
|
||||
import { useSelector } from "react-redux";
|
||||
import { fetchGraphql, gql } from "./utils";
|
||||
import { USE_GRAPHQL } from "./constants";
|
||||
|
||||
async function sendFeedback(variables) {
|
||||
if (USE_GRAPHQL) {
|
||||
const mutation = gql`
|
||||
mutation GiveFeedback($message: String!, $email: String, $url: String) {
|
||||
send_feedback(message: $message, email: $email, url: $url)
|
||||
}
|
||||
`;
|
||||
await fetchGraphql(mutation, variables);
|
||||
} else {
|
||||
await fetch(`${API_URL}/feedback`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(variables),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default function Feedback() {
|
||||
const close = useActionCreator(Actions.closeFeedbackForm);
|
||||
|
|
@ -15,17 +34,14 @@ export default function Feedback() {
|
|||
const [sent, setSent] = useState(false);
|
||||
const url = useSelector(getUrl);
|
||||
const send = useCallback(async () => {
|
||||
const body = { message, email, url: "https://skins/webamp.org" + url };
|
||||
if (message.trim().length === 0) {
|
||||
alert("Please add a message before sending.");
|
||||
return;
|
||||
}
|
||||
const body = { message, email, url: "https://skins/webamp.org" + url };
|
||||
setSending(true);
|
||||
await fetch(`${API_URL}/feedback`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
await sendFeedback(body);
|
||||
|
||||
setSent(true);
|
||||
}, [message, email, url]);
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
import * as Utils from "./utils";
|
||||
import { gql } from "./utils";
|
||||
import TinderCard from "react-tinder-card";
|
||||
import { API_URL } from "./constants";
|
||||
import { API_URL, USE_GRAPHQL } from "./constants";
|
||||
import React, { useState, useEffect } from "react";
|
||||
|
||||
function warmScreenshotImage(hash) {
|
||||
|
|
@ -8,6 +9,34 @@ function warmScreenshotImage(hash) {
|
|||
new Image().src = screenshotUrl;
|
||||
}
|
||||
|
||||
const mutation = gql`
|
||||
query GetSkinToReview {
|
||||
skin_to_review {
|
||||
filename
|
||||
md5
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
async function getSkinToReview() {
|
||||
if (USE_GRAPHQL) {
|
||||
const data = await Utils.fetchGraphql(mutation);
|
||||
return data.skin_to_review;
|
||||
} else {
|
||||
const response = await fetch(
|
||||
`${API_URL}/to_review?cacheBust=${Math.random()}`,
|
||||
{
|
||||
mode: "cors",
|
||||
credentials: "include",
|
||||
}
|
||||
);
|
||||
if (response.status === 403) {
|
||||
window.location = `${API_URL}/auth`;
|
||||
}
|
||||
return response.json();
|
||||
}
|
||||
}
|
||||
|
||||
function useQueuedSkin() {
|
||||
const [queue, setQueue] = useState([]);
|
||||
function remove() {
|
||||
|
|
@ -23,24 +52,13 @@ function useQueuedSkin() {
|
|||
return;
|
||||
}
|
||||
let canceled = false;
|
||||
fetch(`${API_URL}/to_review?cacheBust=${Math.random()}`, {
|
||||
mode: "cors",
|
||||
credentials: "include",
|
||||
})
|
||||
.then((response) => {
|
||||
if (response.status === 403) {
|
||||
window.location = `${API_URL}/auth`;
|
||||
}
|
||||
return response;
|
||||
})
|
||||
.then((response) => response.json())
|
||||
.then((response) => {
|
||||
if (canceled) {
|
||||
return;
|
||||
}
|
||||
warmScreenshotImage(response.md5);
|
||||
setQueue((queue) => [...queue, response]);
|
||||
});
|
||||
getSkinToReview().then((response) => {
|
||||
if (canceled) {
|
||||
return;
|
||||
}
|
||||
warmScreenshotImage(response.md5);
|
||||
setQueue((queue) => [...queue, response]);
|
||||
});
|
||||
|
||||
return () => (canceled = true);
|
||||
}, [queue]);
|
||||
|
|
@ -48,47 +66,75 @@ function useQueuedSkin() {
|
|||
return [queue, remove];
|
||||
}
|
||||
|
||||
async function approveSkin(md5) {
|
||||
if (USE_GRAPHQL) {
|
||||
const mutation = gql`
|
||||
mutation ApproveSkin($md5: String!) {
|
||||
approve_skin(md5: $md5)
|
||||
}
|
||||
`;
|
||||
await Utils.fetchGraphql(mutation, { md5 });
|
||||
} else {
|
||||
await restReview("approve", md5);
|
||||
}
|
||||
}
|
||||
|
||||
async function rejectSkin(md5) {
|
||||
if (USE_GRAPHQL) {
|
||||
const mutation = gql`
|
||||
mutation RejectSkin($md5: String!) {
|
||||
reject_skin(md5: $md5)
|
||||
}
|
||||
`;
|
||||
await Utils.fetchGraphql(mutation, { md5 });
|
||||
} else {
|
||||
await restReview("reject", md5);
|
||||
}
|
||||
}
|
||||
|
||||
async function markSkinNSFW(md5) {
|
||||
if (USE_GRAPHQL) {
|
||||
const mutation = gql`
|
||||
mutation markSkinNSFW($md5: String!) {
|
||||
mark_skin_nsfw(md5: $md5)
|
||||
}
|
||||
`;
|
||||
await Utils.fetchGraphql(mutation, { md5 });
|
||||
} else {
|
||||
await restReview("nsfw", md5);
|
||||
}
|
||||
}
|
||||
|
||||
async function restReview(action, md5) {
|
||||
const response = await fetch(`${API_URL}/skins/${md5}/${action}`, {
|
||||
method: "POST",
|
||||
mode: "cors",
|
||||
credentials: "include",
|
||||
});
|
||||
|
||||
if (response.status === 403) {
|
||||
window.location = `${API_URL}/auth`;
|
||||
}
|
||||
}
|
||||
|
||||
export default function ReviewPage() {
|
||||
const [skins, remove] = useQueuedSkin();
|
||||
async function approve(skin) {
|
||||
remove();
|
||||
const response = await fetch(`${API_URL}/skins/${skin.md5}/approve`, {
|
||||
method: "POST",
|
||||
mode: "cors",
|
||||
credentials: "include",
|
||||
});
|
||||
if (response.status === 403) {
|
||||
window.location = `${API_URL}/auth`;
|
||||
}
|
||||
await approveSkin(skin.md5);
|
||||
}
|
||||
async function reject(skin) {
|
||||
remove();
|
||||
const response = await fetch(`${API_URL}/skins/${skin.md5}/reject`, {
|
||||
method: "POST",
|
||||
mode: "cors",
|
||||
credentials: "include",
|
||||
});
|
||||
|
||||
if (response.status === 403) {
|
||||
window.location = `${API_URL}/auth`;
|
||||
}
|
||||
await rejectSkin(skin.md5);
|
||||
}
|
||||
|
||||
async function nsfw(skin) {
|
||||
remove();
|
||||
const response = await fetch(`${API_URL}/skins/${skin.md5}/nsfw`, {
|
||||
method: "POST",
|
||||
mode: "cors",
|
||||
credentials: "include",
|
||||
});
|
||||
|
||||
if (response.status === 403) {
|
||||
window.location = `${API_URL}/auth`;
|
||||
}
|
||||
await markSkinNSFW(skin.md5);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (skins.lenght === 0) {
|
||||
if (skins.length === 0) {
|
||||
return;
|
||||
}
|
||||
function handleKeypress(e) {
|
||||
|
|
|
|||
|
|
@ -14,15 +14,15 @@ export const REVIEW_PAGE = "REVIEW_PAGE";
|
|||
* 2. Once processed, the server moves them to https://s3.amazonaws.com/cdn.webampskins.org
|
||||
* 3. There is a VPS setup with Varnish which is a proxy in front of https://s3.amazonaws.com/cdn.webampskins.org
|
||||
* 4. https://mirror.webampskins.org is the CloudFlare CDN wich points the the VPS proxy
|
||||
*
|
||||
*
|
||||
* When a request hits CloudFlare it first tries to get it from its local cache.
|
||||
* If it misses, it goes to the VPS. If the VPS is missing the file, it fetches
|
||||
* it from S3.
|
||||
*
|
||||
*
|
||||
* If we ever have trouble with the VPS, we can switch to using
|
||||
* https://cdn.webampskins.org which is just CloudFlare in front of AWS
|
||||
* directly. It's more expensive, but more reliable.
|
||||
*
|
||||
*
|
||||
*/
|
||||
|
||||
// export const CDN = "https://s3.amazonaws.com/webamp-uploaded-skins";
|
||||
|
|
@ -30,7 +30,10 @@ export const REVIEW_PAGE = "REVIEW_PAGE";
|
|||
export const S3_SCREENSHOT_CDN = "https://s3.amazonaws.com/cdn.webampskins.org";
|
||||
export const SCREENSHOT_CDN = "https://mirror.webampskins.org";
|
||||
// mirror. is having some issue with CORs headers that I need to resolve.
|
||||
export const SKIN_CDN = params.vps ? "https://mirror.webampskins.org" : "https://cdn.webampskins.org";
|
||||
export const SKIN_CDN = params.vps
|
||||
? "https://mirror.webampskins.org"
|
||||
: "https://cdn.webampskins.org";
|
||||
export const USE_GRAPHQL = Boolean(params.graphql);
|
||||
// Uncomment these if something goes wrong
|
||||
// export const SCREENSHOT_CDN = "https://cdn.webampskins.org";
|
||||
// export const SKIN_CDN = "https://cdn.webampskins.org";
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@ import { of, from, EMPTY, concat, timer, defer } from "rxjs";
|
|||
import * as Actions from "./actionCreators";
|
||||
import * as Selectors from "./selectors";
|
||||
import * as Utils from "../utils";
|
||||
import { USE_GRAPHQL } from "../constants";
|
||||
import { gql } from "../utils";
|
||||
import {
|
||||
tap,
|
||||
filter,
|
||||
|
|
@ -181,23 +183,56 @@ const chunkState = {};
|
|||
const unloadedSkinEpic = (actions, states) =>
|
||||
actions.pipe(
|
||||
filter((action) => action.type === "REQUEST_UNLOADED_SKIN"),
|
||||
mergeMap(async ({ index }) => {
|
||||
const chunk = Math.floor(index / (CHUNK_SIZE - 1));
|
||||
|
||||
if (chunkState[chunk] != null) {
|
||||
return null;
|
||||
}
|
||||
chunkState[chunk] = "fetching";
|
||||
const response = await fetch(
|
||||
`${API_URL}/skins?offset=${chunk * CHUNK_SIZE}&first=${CHUNK_SIZE}`
|
||||
);
|
||||
|
||||
// TODO: Handle 404
|
||||
|
||||
const body = await response.json();
|
||||
return [body, chunk];
|
||||
map(({ index }) => {
|
||||
return Math.floor(index / (CHUNK_SIZE - 1));
|
||||
}),
|
||||
filter((chunk) => chunkState[chunk] == null),
|
||||
map((chunk) => {
|
||||
chunkState[chunk] = "fetching";
|
||||
const offset = chunk * CHUNK_SIZE;
|
||||
const first = CHUNK_SIZE;
|
||||
return { offset, first, chunk };
|
||||
}),
|
||||
mergeMap(({ offset, first, chunk }) => {
|
||||
if (USE_GRAPHQL) {
|
||||
const query = `
|
||||
query MuseumPage($offset: Int, $first: Int) {
|
||||
skins(offset: $offset, first: $first, sort: MUSEUM) {
|
||||
count
|
||||
nodes {
|
||||
md5
|
||||
filename
|
||||
nsfw
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
return from(Utils.fetchGraphql(query, { offset, first })).pipe(
|
||||
map((data) => {
|
||||
// Map GraphQL data into the format previously returned by REST.
|
||||
return {
|
||||
skinCount: data.skins.count,
|
||||
skins: data.skins.nodes.map((skin) => ({
|
||||
md5: skin.md5,
|
||||
fileName: skin.filename,
|
||||
nsfw: skin.nsfw,
|
||||
})),
|
||||
};
|
||||
}),
|
||||
map((payload) => [payload, chunk])
|
||||
);
|
||||
}
|
||||
|
||||
return from(
|
||||
fetch(
|
||||
`${API_URL}/skins?offset=${chunk * CHUNK_SIZE}&first=${CHUNK_SIZE}`
|
||||
)
|
||||
).pipe(
|
||||
mergeMap((response) => response.json()),
|
||||
map((body) => [body, chunk])
|
||||
);
|
||||
}),
|
||||
filter(Boolean),
|
||||
mergeMap(([body, chunk]) => {
|
||||
return of(
|
||||
{ type: "GOT_SKIN_CHUNK", chunk, payload: body.skins },
|
||||
|
|
@ -430,10 +465,26 @@ const urlEpic = (actions, state) => {
|
|||
return actions.pipe(
|
||||
map(() => Selectors.getUrl(state.value)),
|
||||
distinctUntilChanged(),
|
||||
startWith(window.location),
|
||||
startWith(window.location.pathname),
|
||||
tap((url) => {
|
||||
window.ga("set", "page", url);
|
||||
window.history.replaceState({}, Selectors.getPageTitle(state), url);
|
||||
const currentParams = new URLSearchParams(document.location.search);
|
||||
const proposedUrl = new URL(window.location.origin + url);
|
||||
|
||||
// There are some params that we want to preserve across reloads.
|
||||
for (const key of ["graphql", "vps"]) {
|
||||
let current = currentParams.get(key);
|
||||
if (current == null) {
|
||||
proposedUrl.searchParams.delete(key);
|
||||
} else {
|
||||
proposedUrl.searchParams.set(key, current);
|
||||
}
|
||||
}
|
||||
|
||||
const newUrl = proposedUrl.toString();
|
||||
|
||||
window.ga("set", "page", newUrl);
|
||||
|
||||
window.history.replaceState({}, Selectors.getPageTitle(state), newUrl);
|
||||
}),
|
||||
ignoreElements()
|
||||
);
|
||||
|
|
@ -449,16 +500,37 @@ const skinDataEpic = (actions, state) => {
|
|||
skinData.fileName == null ||
|
||||
skinData.nsfw == null
|
||||
) {
|
||||
return from(fetch(`${API_URL}/skins/${hash}`)).pipe(
|
||||
switchMap((response) => response.json()),
|
||||
map((body) => {
|
||||
return Actions.gotSkinData(hash, {
|
||||
md5: hash,
|
||||
fileName: body.fileName,
|
||||
nsfw: body.nsfw,
|
||||
});
|
||||
})
|
||||
);
|
||||
if (USE_GRAPHQL) {
|
||||
const QUERY = gql`
|
||||
query ($md5: String!) {
|
||||
fetch_skin_by_md5(md5: $md5) {
|
||||
filename
|
||||
nsfw
|
||||
}
|
||||
}
|
||||
`;
|
||||
return from(Utils.fetchGraphql(QUERY, { md5: hash })).pipe(
|
||||
map((data) => {
|
||||
const skin = data.fetch_skin_by_md5;
|
||||
return Actions.gotSkinData(hash, {
|
||||
md5: hash,
|
||||
fileName: skin.fileName,
|
||||
nsfw: skin.nsfw,
|
||||
});
|
||||
})
|
||||
);
|
||||
} else {
|
||||
return from(fetch(`${API_URL}/skins/${hash}`)).pipe(
|
||||
switchMap((response) => response.json()),
|
||||
map((body) => {
|
||||
return Actions.gotSkinData(hash, {
|
||||
md5: hash,
|
||||
fileName: body.fileName,
|
||||
nsfw: body.nsfw,
|
||||
});
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
return EMPTY;
|
||||
})
|
||||
|
|
@ -470,12 +542,21 @@ const markNsfwEpic = (actions) => {
|
|||
filter((action) => action.type === "MARK_NSFW"),
|
||||
mergeMap(async ({ hash }) => {
|
||||
try {
|
||||
const response = await fetch(`${API_URL}/skins/${hash}/report`, {
|
||||
method: "POST",
|
||||
mode: "cors",
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to report skin.");
|
||||
if (USE_GRAPHQL) {
|
||||
const mutation = gql`
|
||||
mutation ReportSkin($md5: String!) {
|
||||
request_nsfw_review_for_skin(md5: $md5)
|
||||
}
|
||||
`;
|
||||
await Utils.fetchGraphql(mutation, { md5: hash });
|
||||
} else {
|
||||
const response = await fetch(`${API_URL}/skins/${hash}/report`, {
|
||||
method: "POST",
|
||||
mode: "cors",
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to report skin.");
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
return Actions.alert(
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import { API_URL } from "../constants";
|
||||
import { API_URL, USE_GRAPHQL } from "../constants";
|
||||
import { gql, fetchGraphql } from "../utils";
|
||||
|
||||
// Upload a skin to S3 and then notify our API that it's ready to process.
|
||||
export async function upload(fileObj) {
|
||||
|
|
@ -38,32 +39,94 @@ export async function upload(fileObj) {
|
|||
// in the DB. For missing skins, we get a URL we can use to upload directly to
|
||||
// S3.
|
||||
export async function getUploadUrls(skins) {
|
||||
const response = await fetch(`${API_URL}/skins/get_upload_urls`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ skins }),
|
||||
});
|
||||
return response.json();
|
||||
if (USE_GRAPHQL) {
|
||||
const files = Object.entries(skins).map(([md5, filename]) => {
|
||||
return {
|
||||
md5,
|
||||
filename,
|
||||
};
|
||||
});
|
||||
const mutation = gql`
|
||||
mutation GetUploadUrls($files: [uplaodRequst!]!) {
|
||||
upload {
|
||||
get_upload_urls(files: $files) {
|
||||
id
|
||||
url
|
||||
md5
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
const data = await fetchGraphql(mutation, { files });
|
||||
const normalized = {};
|
||||
for (const { md5, id, url } in data.upload.get_upload_urls) {
|
||||
normalized[md5] = {
|
||||
id,
|
||||
url,
|
||||
};
|
||||
}
|
||||
return normalized;
|
||||
} else {
|
||||
const response = await fetch(`${API_URL}/skins/get_upload_urls`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ skins }),
|
||||
});
|
||||
return response.json();
|
||||
}
|
||||
}
|
||||
|
||||
// Tell the server that we've uploaded a given skin to S3.
|
||||
export async function reportUploaded(md5, id) {
|
||||
const url = new URL(`${API_URL}/skins/${md5}/uploaded`);
|
||||
url.searchParams.append("id", id);
|
||||
const response = await fetch(url, { method: "POST" });
|
||||
if (!response.ok) {
|
||||
throw new Error("Unable to report skin as uploaded.");
|
||||
if (USE_GRAPHQL) {
|
||||
const mutation = gql`
|
||||
mutation GetUploadUrls($id: String!, $md5: String!) {
|
||||
upload {
|
||||
report_skin_uploaded(id: $id, md5: $md5)
|
||||
}
|
||||
}
|
||||
`;
|
||||
const data = await fetchGraphql(mutation, { id, md5 });
|
||||
if (!data.upload.report_skin_uploaded) {
|
||||
throw new Error("Unable to report skin as uploaded.");
|
||||
}
|
||||
} else {
|
||||
const url = new URL(`${API_URL}/skins/${md5}/uploaded`);
|
||||
url.searchParams.append("id", id);
|
||||
const response = await fetch(url, { method: "POST" });
|
||||
if (!response.ok) {
|
||||
throw new Error("Unable to report skin as uploaded.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Given a list of md5s, ask the server which ones have been fully screenshot etc.
|
||||
export async function checkMd5sUploadStatus(md5s) {
|
||||
const response = await fetch(`${API_URL}/skins/status`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ hashes: md5s }),
|
||||
});
|
||||
return response.json();
|
||||
if (USE_GRAPHQL) {
|
||||
const query = gql`
|
||||
query CheckUploadStatus($md5s: [String!]!) {
|
||||
upload_statuses_by_md5(md5s: $md5s) {
|
||||
upload_md5
|
||||
status
|
||||
}
|
||||
}
|
||||
`;
|
||||
const data = await fetchGraphql(query, { md5s });
|
||||
return data.upload_statuses_by_md5.map((status) => {
|
||||
return {
|
||||
id: status.id,
|
||||
md5: status.upload_md5,
|
||||
status: status.status,
|
||||
};
|
||||
});
|
||||
} else {
|
||||
const response = await fetch(`${API_URL}/skins/status`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ hashes: md5s }),
|
||||
});
|
||||
return response.json();
|
||||
}
|
||||
}
|
||||
|
||||
export async function hashFile(file) {
|
||||
|
|
|
|||
41
src/utils.js
41
src/utils.js
|
|
@ -1,4 +1,4 @@
|
|||
import { SKIN_CDN, SCREENSHOT_CDN } from "./constants";
|
||||
import { SKIN_CDN, SCREENSHOT_CDN, API_URL } from "./constants";
|
||||
|
||||
export function screenshotUrlFromHash(hash) {
|
||||
return `${SCREENSHOT_CDN}/screenshots/${hash}.png`;
|
||||
|
|
@ -91,3 +91,42 @@ export function filenameIsReadme(filename) {
|
|||
].some((name) => filename.match(new RegExp(name, "i")))
|
||||
);
|
||||
}
|
||||
|
||||
// Tools like Prettier can infer that a string is GraphQL if it uses this tagged
|
||||
// template liteal.
|
||||
export function gql(strings) {
|
||||
return strings[0];
|
||||
}
|
||||
|
||||
export async function fetchGraphql(query, variables = {}) {
|
||||
const url = `${API_URL}/graphql`;
|
||||
const response = await fetch(url, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
Accept: "application/json",
|
||||
},
|
||||
mode: "cors",
|
||||
credentials: "include",
|
||||
body: JSON.stringify({ query, variables }),
|
||||
});
|
||||
if (response.status === 403) {
|
||||
window.location = `${API_URL}/auth`;
|
||||
}
|
||||
if (!response.ok) {
|
||||
const payload = await response.text();
|
||||
throw new Error(
|
||||
`GraphQL respose error.
|
||||
URL: ${url}
|
||||
Status: ${response.status}:
|
||||
Respones body:
|
||||
${payload}`
|
||||
);
|
||||
}
|
||||
|
||||
const payload = await response.json();
|
||||
if (payload.errors) {
|
||||
console.warn("GraphQL Response included errors", payload.errors);
|
||||
}
|
||||
return payload.data;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue