anilist-api-wrapper/src/anilist.ts

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"
},
}
},
}
}