import { database } from "$lib/Database/IDB/activities"; import type { User } from "./follow"; import type { AniListAuthorisation, UserIdentity } from "./identity"; export interface ActivityHistoryEntry { date: number; amount: number; } interface ActivityHistoryOptions { stats: { activityHistory: ActivityHistoryEntry[]; }; options: { timezone: string; }; } interface LastActivity { date: Date; timezone: string; } export const fillMissingDays = ( inputActivities: ActivityHistoryEntry[], startOfYear = false, year = new Date().getFullYear(), ): ActivityHistoryEntry[] => { const yearDate = new Date(year, 0, 0); if (inputActivities.length === 0) return startOfYear ? fillDateRange( new Date(yearDate.getUTCFullYear(), 0, 1), new Date(yearDate.getUTCFullYear() + 1, 0, 1), ) : []; const sortedActivities = [...inputActivities].sort((a, b) => a.date - b.date); const endDate = new Date( sortedActivities[sortedActivities.length - 1].date * 1000, ); endDate.setUTCDate(endDate.getUTCDate() + 1); return fillDateRange( startOfYear ? new Date(yearDate.getUTCFullYear(), 0, 1) : new Date(sortedActivities[0].date * 1000), endDate, sortedActivities, ); }; const fillDateRange = ( startDate: Date, endDate: Date, existingActivities: ActivityHistoryEntry[] = [], ): ActivityHistoryEntry[] => { const outputActivities: ActivityHistoryEntry[] = []; for ( let dt = new Date(startDate); dt < endDate; dt.setUTCDate(dt.getUTCDate() + 1) ) { const dateString = dt.toDateString(); if ( !new Set( existingActivities.map((activity) => new Date(activity.date * 1000).toDateString(), ), ).has(dateString) ) { outputActivities.push({ date: Math.floor(dt.getTime() / 1000), amount: 0, }); } else { const activity = existingActivities.find( (activity) => new Date(activity.date * 1000).toDateString() === dateString, ); if (activity) outputActivities.push(activity); } } return outputActivities; }; export const activityHistoryOptions = async ( userIdentity: UserIdentity, authorisation?: AniListAuthorisation, ): Promise => { return ( await ( await fetch("https://graphql.anilist.co", { method: "POST", headers: { "Content-Type": "application/json", Accept: "application/json", ...(authorisation ? { Authorization: `${authorisation.tokenType} ${authorisation.accessToken}`, } : {}), }, body: JSON.stringify({ query: `{ User(id: ${userIdentity.id}) { stats { activityHistory { date amount } } ${authorisation ? "options { timezone }" : ""} } }`, }), }) ).json() )["data"]["User"]; }; const convertToTimezoneOffset = (timeStr: string) => { const [hours, minutes] = timeStr.split(":"); let totalMinutes = parseInt(hours, 10) * 60 + parseInt(minutes, 10); totalMinutes = -totalMinutes; return totalMinutes; }; export const activityHistory = async ( userIdentity: UserIdentity, authorisation?: AniListAuthorisation, ): Promise => (await activityHistoryOptions(userIdentity, authorisation)).stats .activityHistory; export const lastActivityDate = async ( userIdentity: UserIdentity, authorisation: AniListAuthorisation, ): Promise => { if (userIdentity.id === -1 || userIdentity.id === -2) return { date: new Date(8640000000000000), timezone: "" }; const history = await activityHistoryOptions(userIdentity, authorisation); const date = new Date( Number( history.stats.activityHistory[history.stats.activityHistory.length - 1] .date, ) * 1000 + convertToTimezoneOffset(history.options.timezone), ); date.setDate(date.getDate() + 1); return { date, timezone: history.options.timezone }; }; export interface ActivitiesPage { data: { Page: { pageInfo: { hasNextPage: boolean; }; activities: { createdAt: number; }[]; }; }; } const activitiesPage = async ( page: number, anilistAuthorisation: AniListAuthorisation, userIdentity: UserIdentity, year = new Date().getFullYear(), ): Promise => await ( await fetch("https://graphql.anilist.co", { method: "POST", headers: { Authorization: `${anilistAuthorisation.tokenType} ${anilistAuthorisation.accessToken}`, "Content-Type": "application/json", Accept: "application/json", }, body: JSON.stringify({ query: `{ Page(page: ${page}) { pageInfo { hasNextPage } activities(userId: ${userIdentity.id}, createdAt_greater: ${Math.floor( new Date(year, 0, 1).getTime() / 1000, )}, createdAt_lesser: ${Math.floor(new Date(year, 7, 1).getTime() / 1000)}) { ... on TextActivity { createdAt } ... on ListActivity { createdAt } ... on MessageActivity { createdAt } } } }`, }), }) ).json(); export const fullActivityHistory = async ( anilistAuthorisation: AniListAuthorisation, userIdentity: UserIdentity, year = new Date().getFullYear(), disableLoopingActivityCounter = false, ): Promise => { const activities = []; let page = 1; let currentDatabasePage = await database.activities.get(page); let currentPage: ActivitiesPage; if (currentDatabasePage) currentPage = currentDatabasePage.data; else { currentPage = await activitiesPage( page, anilistAuthorisation, userIdentity, year, ); database.activities.add({ page, data: currentPage, }); } for (const activity of currentPage.data.Page.activities) activities.push(activity); while (currentPage["data"]["Page"]["pageInfo"]["hasNextPage"]) { if (disableLoopingActivityCounter) break; for (const activity of currentPage.data.Page.activities) activities.push(activity); page += 1; currentDatabasePage = await database.activities.get(page); if (currentDatabasePage) currentPage = currentDatabasePage.data; else { currentPage = await activitiesPage( page, anilistAuthorisation, userIdentity, year, ); database.activities.add({ page, data: currentPage, }); } } for (const activity of currentPage.data.Page.activities) activities.push(activity); let fullLocalActivityHistory: ActivityHistoryEntry[] = []; for (const activity of activities) { const date = new Date(activity.createdAt * 1000); const dateString = date.toDateString(); const activityHistoryEntry = fullLocalActivityHistory.find( (activityHistoryEntry) => new Date(activityHistoryEntry.date * 1000).toDateString() === dateString, ); if (activityHistoryEntry) activityHistoryEntry.amount += 1; else fullLocalActivityHistory.push({ date: Math.floor(date.getTime() / 1000), amount: 1, }); } fullLocalActivityHistory = fullLocalActivityHistory.filter( (a) => !isNaN(a.date), ); if (new Date().getMonth() > 6) fullLocalActivityHistory.push(...(await activityHistory(userIdentity))); fullLocalActivityHistory = fullLocalActivityHistory.filter( (activityHistoryEntry, index, self) => self.findIndex( (a) => new Date(a.date * 1000).toDateString() === new Date(activityHistoryEntry.date * 1000).toDateString(), ) === index, ); fullLocalActivityHistory = fullLocalActivityHistory.filter( (activityHistoryEntry) => new Date(activityHistoryEntry.date * 1000).getFullYear() === year, ); return fullLocalActivityHistory; }; export const activityLikes = async (id: number): Promise[]> => { const activityResponse = await ( await fetch("https://graphql.anilist.co", { method: "POST", headers: { "Content-Type": "application/json", Accept: "application/json", }, body: JSON.stringify({ query: `{ Activity(id: ${id}) { __typename ... on TextActivity { likes { name avatar { large } } } ... on ListActivity { likes { name avatar { large } } } ... on MessageActivity { likes { name avatar { large } } } } }`, }), }) ).json(); return activityResponse["data"]["Activity"]["likes"]; }; export const activityText = async ( id: number, replies = false, ): Promise => { const activityResponse = await ( await fetch("https://graphql.anilist.co", { method: "POST", headers: { "Content-Type": "application/json", Accept: "application/json", }, body: JSON.stringify({ query: `{ Activity(id: ${id}) { ... on TextActivity { text(asHtml: true) ${replies ? "replies { text(asHtml: true) }" : ""} } } }`, }), }) ).json(); let text = activityResponse["data"]["Activity"]["text"]; if (replies) for (const reply of activityResponse["data"]["Activity"]["replies"]) text += reply.text; return text; };