672 lines
18 KiB
TypeScript
672 lines
18 KiB
TypeScript
import { Fetch, MediaListArguments, ThreadCommentSort, UpdateQuery, Variables } from "./types/Anilist.ts"
|
|
import type { AtLeastOne } from "./types/AtLeastOne.ts"
|
|
|
|
const typeMap = {
|
|
User: { sort: "[UserSort]" },
|
|
about: { asHtml: "Boolean" },
|
|
releaseYear: { sort: "[UserStatisticsSort]" },
|
|
misc: {
|
|
page: "Int",
|
|
perPage: "Int",
|
|
id: "Int",
|
|
userId: "Int",
|
|
comment: "String",
|
|
name: "String",
|
|
userName: "String",
|
|
isModerator: "Boolean",
|
|
search: "String",
|
|
limit: "Int",
|
|
format: "ScoreFormat",
|
|
asArray: "Boolean",
|
|
mediaId: "Int",
|
|
},
|
|
}
|
|
|
|
const tab = (n: number) => new Array(n + 1).fill("").join("\t")
|
|
const t = tab
|
|
|
|
const updateOperation = (
|
|
{ query, variables, field, level, hasSubField }: UpdateQuery,
|
|
) => {
|
|
if (!query) query = `${field[level]} (%root) {\n\t%${field[level]}\n}`
|
|
const convertedType: [string[], string[]] = [[], []]
|
|
if (variables) {
|
|
const variable = Object.entries(variables)
|
|
for (let i = 0; i <= variable.length - 1; i++) {
|
|
convertedType[0].push(`${variable[i][0]}: $${variable[i][0]}`)
|
|
convertedType[1].push(
|
|
`$${variable[i][0]}: ${
|
|
variable[i][0] in typeMap
|
|
? typeMap[variable[i][0] as keyof typeof typeMap]
|
|
: typeMap["misc"][variable[i][0] as keyof typeof typeMap["misc"]]
|
|
}`,
|
|
)
|
|
}
|
|
query = query.replace(
|
|
`%root`,
|
|
`${convertedType[1].join(", ")}, %root`,
|
|
)
|
|
}
|
|
query = query
|
|
.replace(
|
|
`%${field[level - 1]}`,
|
|
`${field[level]}${variables ? ` (${convertedType[0].join(", ")})` : ""}${
|
|
hasSubField ? `${` {\n${t(level + 1)}%${field.at(-1)}\n${t(level)}}`}` : ""
|
|
}\n${t(level)}%${field[level - 1]}`,
|
|
)
|
|
// console.log({ query, variables, field, level })
|
|
|
|
return {
|
|
get() {
|
|
// return query
|
|
return query!.replace(/(\n|,)?(\t+| )%\w+\b/g, "")
|
|
},
|
|
set(
|
|
{ subField, variables, hasSubField, level }:
|
|
& Pick<
|
|
UpdateQuery,
|
|
"hasSubField" | "variables"
|
|
>
|
|
& { subField: string; level: number },
|
|
) {
|
|
// const f = [...field]
|
|
field.splice(level, 0, subField)
|
|
while (field.length - 1 > level) field.pop()
|
|
|
|
return updateOperation({
|
|
query,
|
|
variables,
|
|
// level: depth ? depth : level + 1,
|
|
level,
|
|
field,
|
|
hasSubField,
|
|
})
|
|
},
|
|
}
|
|
}
|
|
|
|
const fetch: Fetch = async (init) => {
|
|
let host:
|
|
| "https://graphql.anilist.co"
|
|
| "https://anilist.co/api/v2/oauth/token"
|
|
const body = {}
|
|
const headers = new Headers({
|
|
"Content-Type": "application/json",
|
|
"Accept": "application/json",
|
|
})
|
|
if ("code" in init) {
|
|
const {
|
|
client_id,
|
|
client_secret,
|
|
code,
|
|
redirect_uri,
|
|
grant_type = "authorization_code",
|
|
} = init
|
|
host = "https://anilist.co/api/v2/oauth/token"
|
|
Object.assign(body, {
|
|
grant_type,
|
|
client_id,
|
|
client_secret,
|
|
redirect_uri,
|
|
code,
|
|
})
|
|
} else {
|
|
host = "https://graphql.anilist.co"
|
|
const { query, variables, token } = init
|
|
Object.assign(body, { query, variables })
|
|
if (token) headers.set("Authorization", `Bearer ${token}`)
|
|
}
|
|
const res = await self.fetch(host, {
|
|
method: "POST",
|
|
headers,
|
|
body: JSON.stringify(body),
|
|
})
|
|
if (res.status !== 200) {
|
|
throw new Error("Something went wrong.", {
|
|
cause: JSON.stringify(await res.json()),
|
|
})
|
|
}
|
|
return await res.json()
|
|
}
|
|
|
|
const fuzzyDateFields = (
|
|
query: ReturnType<typeof updateOperation>[],
|
|
level: number,
|
|
) => ({
|
|
withDay() {
|
|
query[0] = query[0].set({ subField: "day", level })
|
|
return this
|
|
},
|
|
withMonth() {
|
|
query[0] = query[0].set({ subField: "month", level })
|
|
return this
|
|
},
|
|
withYear() {
|
|
query[0] = query[0].set({ subField: "year", level })
|
|
return this
|
|
},
|
|
})
|
|
const MediaListFields = (
|
|
query: ReturnType<typeof updateOperation>[],
|
|
_level: number,
|
|
) => ({
|
|
withCompletedAt(fn: (fields: ReturnType<typeof fuzzyDateFields>) => void) {
|
|
query[0] = query[0].set({
|
|
subField: "completedAt",
|
|
level: 2,
|
|
hasSubField: true,
|
|
})
|
|
let tmpQuery
|
|
fn(fuzzyDateFields(tmpQuery = [query[0]], 3))
|
|
query[0] = tmpQuery[0]
|
|
return this
|
|
},
|
|
withStartedAt(fn: (fields: ReturnType<typeof fuzzyDateFields>) => void) {
|
|
query[0] = query[0].set({
|
|
subField: "startedAt",
|
|
level: 2,
|
|
hasSubField: true,
|
|
})
|
|
let tmpQuery
|
|
fn(fuzzyDateFields(tmpQuery = [query[0]], 3))
|
|
query[0] = tmpQuery[0]
|
|
return this
|
|
},
|
|
withId() {
|
|
query[0] = query[0].set({ subField: "id", level: 2 })
|
|
return this
|
|
},
|
|
withUserId() {
|
|
query[0] = query[0].set({ subField: "userId", level: 2 })
|
|
return this
|
|
},
|
|
withMediaId() {
|
|
query[0] = query[0].set({ subField: "mediaId", level: 2 })
|
|
return this
|
|
},
|
|
withStatus() {
|
|
query[0] = query[0].set({ subField: "status", level: 2 })
|
|
return this
|
|
},
|
|
withProgress() {
|
|
query[0] = query[0].set({ subField: "progress", level: 2 })
|
|
return this
|
|
},
|
|
withProgressVolumes() {
|
|
query[0] = query[0].set({ subField: "progressVolumes", level: 2 })
|
|
return this
|
|
},
|
|
withRepeat() {
|
|
query[0] = query[0].set({ subField: "repeat", level: 2 })
|
|
return this
|
|
},
|
|
withPriority() {
|
|
query[0] = query[0].set({ subField: "priority", level: 2 })
|
|
return this
|
|
},
|
|
withPrivate() {
|
|
query[0] = query[0].set({ subField: "private", level: 2 })
|
|
return this
|
|
},
|
|
withNotes() {
|
|
query[0] = query[0].set({ subField: "notes", level: 2 })
|
|
return this
|
|
},
|
|
withAdvancedScores() {
|
|
query[0] = query[0].set({ subField: "advancedScores", level: 2 })
|
|
return this
|
|
},
|
|
withHiddenFromStatusLists() {
|
|
query[0] = query[0].set({
|
|
subField: "hiddenFromStatusLists",
|
|
level: 2,
|
|
})
|
|
return this
|
|
},
|
|
withUpdatedAt() {
|
|
query[0] = query[0].set({ subField: "updatedAt", level: 2 })
|
|
return this
|
|
},
|
|
withCreatedAt() {
|
|
query[0] = query[0].set({ subField: "createdAt", level: 2 })
|
|
return this
|
|
},
|
|
})
|
|
const threadCommentFields = (
|
|
query: ReturnType<typeof updateOperation>[],
|
|
level: number,
|
|
) => ({
|
|
/** The id of the comment */
|
|
withId() {
|
|
query[0] = query[0].set({ subField: "id", level })
|
|
return this
|
|
},
|
|
/** The user id of the comment's owner */
|
|
withUserId() {
|
|
query[0] = query[0].set({ subField: "userId", level })
|
|
return this
|
|
},
|
|
/** The id of thread the comment belongs to */
|
|
withThreadId() {
|
|
query[0] = query[0].set({ subField: "threadId", level })
|
|
return this
|
|
},
|
|
/** The text content of the comment (Markdown) */
|
|
withComment(args?: { asHtml: boolean }) {
|
|
query[0] = query[0].set({ subField: "comment", level, variables: args })
|
|
return this
|
|
},
|
|
/** The amount of likes the comment has */
|
|
withLikeCount() {
|
|
query[0] = query[0].set({ subField: "likeCount", level })
|
|
return this
|
|
},
|
|
/** If the currently authenticated user liked the comment */
|
|
withIsLiked() {
|
|
query[0] = query[0].set({ subField: "isLiked", level })
|
|
return this
|
|
},
|
|
/** The url for the comment page on the AniList website */
|
|
withSiteUrl() {
|
|
query[0] = query[0].set({ subField: "siteUrl", level })
|
|
return this
|
|
},
|
|
/** The time of the comments creation */
|
|
withCreatedAt() {
|
|
query[0] = query[0].set({ subField: "createdAt", level })
|
|
return this
|
|
},
|
|
/** The time of the comments last update */
|
|
withUpdatedAt() {
|
|
query[0] = query[0].set({ subField: "updatedAt", level })
|
|
return this
|
|
},
|
|
/** The thread the comment belongs to */
|
|
withThread() {
|
|
query[0] = query[0].set({ subField: "thread", level })
|
|
return this
|
|
},
|
|
/** The user who created the comment */
|
|
withUser() {
|
|
query[0] = query[0].set({ subField: "user", level })
|
|
return this
|
|
},
|
|
/** The users who liked the comment */
|
|
withLikes() {
|
|
query[0] = query[0].set({ subField: "likes", level })
|
|
return this
|
|
},
|
|
/** The comment's child reply comments */
|
|
withChildComments() {
|
|
query[0] = query[0].set({ subField: "childComments", level })
|
|
return this
|
|
},
|
|
/** If the comment tree is locked and may not receive replies or edits */
|
|
withIsLocked() {
|
|
query[0] = query[0].set({ subField: "isLocked", level })
|
|
return this
|
|
},
|
|
})
|
|
|
|
export const Client = function (auth?: { token: string }) {
|
|
const variables: Variables = {}
|
|
let operation: ReturnType<typeof updateOperation>
|
|
return {
|
|
get auth() {
|
|
return {
|
|
getToken(init: {
|
|
code: string
|
|
client_id: string
|
|
client_secret: string
|
|
redirect_uri: string
|
|
}) {
|
|
return fetch(init)
|
|
},
|
|
}
|
|
},
|
|
get query() {
|
|
operation = updateOperation({
|
|
field: ["query"],
|
|
level: 0,
|
|
})
|
|
|
|
return {
|
|
get raw() {
|
|
return {
|
|
get variable() {
|
|
return variables
|
|
},
|
|
get() {
|
|
return operation.get()
|
|
},
|
|
}
|
|
},
|
|
fetch() {
|
|
return fetch({ query: this.raw.get()!, variables })
|
|
},
|
|
Page() {
|
|
throw "To be Implemented"
|
|
},
|
|
/** Media query */
|
|
Media() {
|
|
throw "To be Implemented"
|
|
},
|
|
/** Media Trend query */
|
|
MediaTrend() {
|
|
throw "To be Implemented"
|
|
},
|
|
/** Airing schedule query */
|
|
AiringSchedule() {
|
|
throw "To be Implemented"
|
|
},
|
|
/** Character query */
|
|
Character() {
|
|
throw "To be Implemented"
|
|
},
|
|
/** Staff query */
|
|
Staff() {
|
|
throw "To be Implemented"
|
|
},
|
|
/** Media list query */
|
|
MediaList(
|
|
args: AtLeastOne<MediaListArguments> & Variables,
|
|
fn?: (fields: ReturnType<typeof MediaListFields>) => void,
|
|
) {
|
|
Object.assign(variables, args)
|
|
operation = operation.set({
|
|
subField: "MediaList",
|
|
variables: args,
|
|
hasSubField: true,
|
|
level: 1,
|
|
})
|
|
if (!fn) operation = operation.set({ subField: "id", level: 2 })
|
|
else {
|
|
let tmpQuery
|
|
fn(MediaListFields(tmpQuery = [operation], 2))
|
|
operation = tmpQuery[0]
|
|
}
|
|
return this
|
|
},
|
|
/** Media list collection query, provides list pre-grouped by status & custom lists. User ID and Media Type arguments required. */
|
|
MediaListCollection() {
|
|
throw "To be Implemented"
|
|
},
|
|
/** Collection of all the possible media genres */
|
|
GenreCollection() {
|
|
throw "To be Implemented"
|
|
},
|
|
/** Collection of all the possible media tags */
|
|
MediaTagCollection() {
|
|
throw "To be Implemented"
|
|
},
|
|
/** User query */
|
|
User() {
|
|
throw "To be Implemented"
|
|
},
|
|
/** Get the currently authenticated user */
|
|
Viewer() {
|
|
throw "To be Implemented"
|
|
},
|
|
/** Notification query */
|
|
Notification() {
|
|
throw "To be Implemented"
|
|
},
|
|
/** Studio query */
|
|
Studio() {
|
|
throw "To be Implemented"
|
|
},
|
|
/** Review query */
|
|
Review() {
|
|
throw "To be Implemented"
|
|
},
|
|
/** Activity query */
|
|
Activity() {
|
|
throw "To be Implemented"
|
|
},
|
|
/** Activity reply query */
|
|
ActivityReply() {
|
|
throw "To be Implemented"
|
|
},
|
|
/** Follow query */
|
|
Following() {
|
|
throw "To be Implemented"
|
|
},
|
|
/** Follow query */
|
|
Follower() {
|
|
throw "To be Implemented"
|
|
},
|
|
/** Thread query */
|
|
Thread() {
|
|
throw "To be Implemented"
|
|
},
|
|
/** Comment query */
|
|
ThreadComment(
|
|
args: AtLeastOne<
|
|
{
|
|
/** Filter by the comment id */
|
|
id: number
|
|
/** Filter by the thread id */
|
|
threadId: number
|
|
/** Filter by the user id of the comment's creator */
|
|
userId: number
|
|
/** The order the results will be returned in */
|
|
sort: ThreadCommentSort[]
|
|
} & Variables
|
|
>,
|
|
fn?: (fields: ReturnType<typeof threadCommentFields>) => void,
|
|
) {
|
|
Object.assign(variables, args)
|
|
operation = operation.set({
|
|
level: 1,
|
|
subField: "ThreadComment",
|
|
hasSubField: true,
|
|
variables: args,
|
|
})
|
|
if (!fn) operation = operation.set({ level: 2, subField: "id" })
|
|
else {
|
|
let tmpQuery
|
|
fn(threadCommentFields(tmpQuery = [operation], 2))
|
|
operation = tmpQuery[0]
|
|
}
|
|
return this
|
|
},
|
|
/** Recommendation query */
|
|
Recommendation() {
|
|
throw "To be Implemented"
|
|
},
|
|
/** Like query */
|
|
Like() {
|
|
throw "To be Implemented"
|
|
},
|
|
/** Provide AniList markdown to be converted to html (Requires auth) */
|
|
Markdown() {
|
|
throw "To be Implemented"
|
|
},
|
|
AniChartUser() {
|
|
throw "To be Implemented"
|
|
},
|
|
/** Site statistics query */
|
|
SiteStatistics() {
|
|
throw "To be Implemented"
|
|
},
|
|
/** ExternalLinkSource collection query */
|
|
ExternalLinkSourceCollection() {
|
|
throw "To be Implemented"
|
|
},
|
|
}
|
|
},
|
|
get mutation() {
|
|
operation = updateOperation({
|
|
field: ["mutation"],
|
|
level: 0,
|
|
})
|
|
|
|
return {
|
|
get raw() {
|
|
return {
|
|
get variable() {
|
|
return variables
|
|
},
|
|
get() {
|
|
return operation.get()
|
|
},
|
|
}
|
|
},
|
|
fetch() {
|
|
return fetch({
|
|
query: this.raw.get()!,
|
|
variables,
|
|
token: auth?.token,
|
|
})
|
|
},
|
|
/** Update current user options */
|
|
UpdateUser() {
|
|
throw "To be Implemented"
|
|
},
|
|
/** Create or update a media list entry */
|
|
SaveMediaListEntry() {
|
|
throw "To be Implemented"
|
|
},
|
|
/** Update multiple media list entries to the same values */
|
|
UpdateMediaListEntries() {
|
|
throw "To be Implemented"
|
|
},
|
|
/** Delete a media list entry */
|
|
DeleteMediaListEntry() {
|
|
throw "To be Implemented"
|
|
},
|
|
/** Delete a custom list and remove the list entries from it */
|
|
DeleteCustomList() {
|
|
throw "To be Implemented"
|
|
},
|
|
/** Create or update text activity for the currently authenticated user */
|
|
SaveTextActivity() {
|
|
throw "To be Implemented"
|
|
},
|
|
/** Create or update message activity for the currently authenticated user */
|
|
SaveMessageActivity() {
|
|
throw "To be Implemented"
|
|
},
|
|
/** Update list activity (Mod Only) */
|
|
SaveListActivity() {
|
|
throw "To be Implemented"
|
|
},
|
|
/** Delete an activity item of the authenticated users */
|
|
DeleteActivity() {
|
|
throw "To be Implemented"
|
|
},
|
|
/** Toggle activity to be pinned to the top of the user's activity feed */
|
|
ToggleActivityPin() {
|
|
throw "To be Implemented"
|
|
},
|
|
/** Toggle the subscription of an activity item */
|
|
ToggleActivitySubscription() {
|
|
throw "To be Implemented"
|
|
},
|
|
/** Create or update an activity reply */
|
|
SaveActivityReply() {
|
|
throw "To be Implemented"
|
|
},
|
|
/** Delete an activity reply of the authenticated users */
|
|
DeleteActivityReply() {
|
|
throw "To be Implemented"
|
|
},
|
|
/** Add or remove a like from a likeable type. Returns all the users who liked the same model */
|
|
ToggleLike() {
|
|
throw "To be Implemented"
|
|
},
|
|
/** Add or remove a like from a likeable type. */
|
|
ToggleLikeV2() {
|
|
throw "To be Implemented"
|
|
},
|
|
/** Toggle the un/following of a user */
|
|
ToggleFollow() {
|
|
throw "To be Implemented"
|
|
},
|
|
/** Favourite or unfavourite an anime, manga, character, staff member, or studio */
|
|
ToggleFavourite() {
|
|
throw "To be Implemented"
|
|
},
|
|
/** Update the order favourites are displayed in */
|
|
UpdateFavouriteOrder() {
|
|
throw "To be Implemented"
|
|
},
|
|
/** Create or update a review */
|
|
SaveReview() {
|
|
throw "To be Implemented"
|
|
},
|
|
/** Delete a review */
|
|
DeleteReview() {
|
|
throw "To be Implemented"
|
|
},
|
|
/** Rate a review */
|
|
RateReview() {
|
|
throw "To be Implemented"
|
|
},
|
|
/** Recommendation a media */
|
|
SaveRecommendation() {
|
|
throw "To be Implemented"
|
|
},
|
|
/** Create or update a forum thread */
|
|
SaveThread() {
|
|
throw "To be Implemented"
|
|
},
|
|
/** Delete a thread */
|
|
DeleteThread() {
|
|
throw "To be Implemented"
|
|
},
|
|
/** Toggle the subscription of a forum thread */
|
|
ToggleThreadSubscription() {
|
|
throw "To be Implemented"
|
|
},
|
|
/** Create or update a thread comment */
|
|
SaveThreadComment(
|
|
args:
|
|
& ({
|
|
/** The comment id, required for updating */
|
|
id: number
|
|
} | {
|
|
/** The id of thread the comment belongs to */
|
|
threadId: number
|
|
} | {
|
|
/** The id of thread comment to reply to */
|
|
parentCommentId: number
|
|
})
|
|
& {
|
|
/** The comment markdown text */
|
|
comment: string
|
|
/** If the comment tree should be locked. (Mod Only) */
|
|
locked?: boolean
|
|
},
|
|
fn?: (fields: ReturnType<typeof threadCommentFields>) => void,
|
|
) {
|
|
Object.assign(variables, args)
|
|
operation = operation.set({
|
|
level: 1,
|
|
subField: "SaveThreadComment",
|
|
hasSubField: true,
|
|
variables: args,
|
|
})
|
|
if (!fn) operation = operation.set({ level: 2, subField: "id" })
|
|
else {
|
|
let tmpQuery
|
|
fn(threadCommentFields(tmpQuery = [operation], 2))
|
|
operation = tmpQuery[0]
|
|
}
|
|
return this
|
|
},
|
|
/** Delete a thread comment */
|
|
DeleteThreadComment() {
|
|
throw "To be Implemented"
|
|
},
|
|
UpdateAniChartSettings() {
|
|
throw "To be Implemented"
|
|
},
|
|
UpdateAniChartHighlights() {
|
|
throw "To be Implemented"
|
|
},
|
|
}
|
|
},
|
|
}
|
|
}
|