From eab530a63dd0de7488028082eddfb6b7277beca7 Mon Sep 17 00:00:00 2001 From: Puyodead1 Date: Fri, 13 Jan 2023 08:54:09 -0500 Subject: Add Youtube connection --- src/connections/Youtube/index.ts | 143 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 143 insertions(+) create mode 100644 src/connections/Youtube/index.ts (limited to 'src/connections/Youtube/index.ts') diff --git a/src/connections/Youtube/index.ts b/src/connections/Youtube/index.ts new file mode 100644 index 00000000..afc9356b --- /dev/null +++ b/src/connections/Youtube/index.ts @@ -0,0 +1,143 @@ +import { + Config, + ConnectedAccount, + ConnectedAccountCommonOAuthTokenResponse, + ConnectionCallbackSchema, + ConnectionLoader, + DiscordApiErrors, +} from "@fosscord/util"; +import wretch from "wretch"; +import Connection from "../../util/connections/Connection"; +import { YoutubeSettings } from "./YoutubeSettings"; + +interface YouTubeConnectionChannelListResult { + items: { + snippet: { + // thumbnails: Thumbnails; + title: string; + country: string; + publishedAt: string; + // localized: Localized; + description: string; + }; + kind: string; + etag: string; + id: string; + }[]; + kind: string; + etag: string; + pageInfo: { + resultsPerPage: number; + totalResults: number; + }; +} + +export default class YoutubeConnection extends Connection { + public readonly id = "youtube"; + public readonly authorizeUrl = + "https://accounts.google.com/o/oauth2/v2/auth"; + public readonly tokenUrl = "https://oauth2.googleapis.com/token"; + public readonly userInfoUrl = + "https://www.googleapis.com/youtube/v3/channels?mine=true&part=snippet"; + public readonly scopes = [ + "https://www.googleapis.com/auth/youtube.readonly", + ]; + settings: YoutubeSettings = new YoutubeSettings(); + + init(): void { + this.settings = ConnectionLoader.getConnectionConfig( + this.id, + this.settings, + ) as YoutubeSettings; + } + + getAuthorizationUrl(userId: string): string { + const state = this.createState(userId); + const url = new URL(this.authorizeUrl); + + url.searchParams.append("client_id", this.settings.clientId!); + // TODO: probably shouldn't rely on cdn as this could be different from what we actually want. we should have an api endpoint setting. + url.searchParams.append( + "redirect_uri", + `${ + Config.get().cdn.endpointPrivate || "http://localhost:3001" + }/connections/${this.id}/callback`, + ); + url.searchParams.append("response_type", "code"); + url.searchParams.append("scope", this.scopes.join(" ")); + url.searchParams.append("state", state); + return url.toString(); + } + + getTokenUrl(): string { + return this.tokenUrl; + } + + async exchangeCode( + state: string, + code: string, + ): Promise { + this.validateState(state); + + const url = this.getTokenUrl(); + + return wretch(url.toString()) + .headers({ + Accept: "application/json", + "Content-Type": "application/x-www-form-urlencoded", + }) + .body( + new URLSearchParams({ + grant_type: "authorization_code", + code: code, + client_id: this.settings.clientId!, + client_secret: this.settings.clientSecret!, + redirect_uri: `${ + Config.get().cdn.endpointPrivate || + "http://localhost:3001" + }/connections/${this.id}/callback`, + }), + ) + .post() + .json() + .catch((e) => { + console.error(e); + throw DiscordApiErrors.GENERAL_ERROR; + }); + } + + async getUser(token: string): Promise { + const url = new URL(this.userInfoUrl); + return wretch(url.toString()) + .headers({ + Authorization: `Bearer ${token}`, + }) + .get() + .json() + .catch((e) => { + console.error(e); + throw DiscordApiErrors.GENERAL_ERROR; + }); + } + + async handleCallback( + params: ConnectionCallbackSchema, + ): Promise { + const userId = this.getUserId(params.state); + const tokenData = await this.exchangeCode(params.state, params.code!); + const userInfo = await this.getUser(tokenData.access_token); + + const exists = await this.hasConnection(userId, userInfo.items[0].id); + + if (exists) return null; + + return await this.createConnection({ + token_data: { ...tokenData, fetched_at: Date.now() }, + user_id: userId, + external_id: userInfo.items[0].id, + friend_sync: params.friend_sync, + name: userInfo.items[0].snippet.title, + type: this.id, + }); + } +} -- cgit 1.5.1 From c7277efbad5d3979222518ae543366ba8a08ca77 Mon Sep 17 00:00:00 2001 From: Madeline <46743919+MaddyUnderStars@users.noreply.github.com> Date: Tue, 24 Jan 2023 18:15:26 +1100 Subject: Move redirect uri generation to getRedirectUri function of Connection class. Use api_endpointPublic instead of cdn_endpointPublic --- package-lock.json | 5 +++++ .../#connection_name/#connection_id/index.ts | 12 +++++++----- src/connections/BattleNet/index.ts | 14 ++------------ src/connections/Discord/index.ts | 15 ++------------- src/connections/EpicGames/index.ts | 9 +-------- src/connections/Facebook/index.ts | 16 ++-------------- src/connections/GitHub/index.ts | 9 +-------- src/connections/Reddit/index.ts | 14 ++------------ src/connections/Spotify/index.ts | 14 ++------------ src/connections/Twitch/index.ts | 14 ++------------ src/connections/Twitter/index.ts | 19 +++---------------- src/connections/Xbox/index.ts | 14 ++------------ src/connections/Youtube/index.ts | 14 ++------------ src/util/config/types/ApiConfiguration.ts | 2 +- src/util/connections/Connection.ts | 12 +++++++++++- 15 files changed, 45 insertions(+), 138 deletions(-) (limited to 'src/connections/Youtube/index.ts') diff --git a/package-lock.json b/package-lock.json index 8947d9e4..db3cbd8a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14178,6 +14178,11 @@ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" }, + "wretch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/wretch/-/wretch-2.3.2.tgz", + "integrity": "sha512-brN97Z2Mwed+w5z+keYI1u5OwWhPIaW0sJi9CxtKBVxLc3aqP6j1+2FCoIskM7WJq6SUHdxTFx20ox0iDLa0mQ==" + }, "ws": { "version": "8.11.0", "resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz", diff --git a/src/api/routes/users/@me/connections/#connection_name/#connection_id/index.ts b/src/api/routes/users/@me/connections/#connection_name/#connection_id/index.ts index 07440eac..5b8936f0 100644 --- a/src/api/routes/users/@me/connections/#connection_name/#connection_id/index.ts +++ b/src/api/routes/users/@me/connections/#connection_name/#connection_id/index.ts @@ -3,7 +3,7 @@ import { ConnectedAccount, ConnectionUpdateSchema, DiscordApiErrors, - emitEvent + emitEvent, } from "@fosscord/util"; import { Request, Response, Router } from "express"; const router = Router(); @@ -38,10 +38,12 @@ router.patch( if (!connection) return DiscordApiErrors.UNKNOWN_CONNECTION; // TODO: do we need to do anything if the connection is revoked? - //@ts-ignore For some reason the client sends this as a boolean, even tho docs say its a number? - if (typeof body.visibility === "boolean") body.visibility = body.visibility ? 1 : 0; - //@ts-ignore For some reason the client sends this as a boolean, even tho docs say its a number? - if (typeof body.show_activity === "boolean") body.show_activity = body.show_activity ? 1 : 0; + if (typeof body.visibility === "boolean") + //@ts-expect-error For some reason the client sends this as a boolean, even tho docs say its a number? + body.visibility = body.visibility ? 1 : 0; + if (typeof body.show_activity === "boolean") + //@ts-expect-error For some reason the client sends this as a boolean, even tho docs say its a number? + body.show_activity = body.show_activity ? 1 : 0; connection.assign(req.body); diff --git a/src/connections/BattleNet/index.ts b/src/connections/BattleNet/index.ts index 96c3993c..a88633ab 100644 --- a/src/connections/BattleNet/index.ts +++ b/src/connections/BattleNet/index.ts @@ -1,5 +1,4 @@ import { - Config, ConnectedAccount, ConnectedAccountCommonOAuthTokenResponse, ConnectionCallbackSchema, @@ -41,13 +40,7 @@ export default class BattleNetConnection extends Connection { const url = new URL(this.authorizeUrl); url.searchParams.append("client_id", this.settings.clientId!); - // TODO: probably shouldn't rely on cdn as this could be different from what we actually want. we should have an api endpoint setting. - url.searchParams.append( - "redirect_uri", - `${ - Config.get().cdn.endpointPrivate || "http://localhost:3001" - }/connections/${this.id}/callback`, - ); + url.searchParams.append("redirect_uri", this.getRedirectUri()); url.searchParams.append("scope", this.scopes.join(" ")); url.searchParams.append("state", state); url.searchParams.append("response_type", "code"); @@ -76,10 +69,7 @@ export default class BattleNetConnection extends Connection { code: code, client_id: this.settings.clientId!, client_secret: this.settings.clientSecret!, - redirect_uri: `${ - Config.get().cdn.endpointPrivate || - "http://localhost:3001" - }/connections/${this.id}/callback`, + redirect_uri: this.getRedirectUri(), }), ) .post() diff --git a/src/connections/Discord/index.ts b/src/connections/Discord/index.ts index 52fc9ffd..1f812e4d 100644 --- a/src/connections/Discord/index.ts +++ b/src/connections/Discord/index.ts @@ -1,5 +1,4 @@ import { - Config, ConnectedAccount, ConnectedAccountCommonOAuthTokenResponse, ConnectionCallbackSchema, @@ -42,14 +41,7 @@ export default class DiscordConnection extends Connection { url.searchParams.append("response_type", "code"); // controls whether, on repeated authorizations, the consent screen is shown url.searchParams.append("consent", "none"); - - // TODO: probably shouldn't rely on cdn as this could be different from what we actually want. we should have an api endpoint setting. - url.searchParams.append( - "redirect_uri", - `${ - Config.get().cdn.endpointPrivate || "http://localhost:3001" - }/connections/${this.id}/callback`, - ); + url.searchParams.append("redirect_uri", this.getRedirectUri()); return url.toString(); } @@ -76,10 +68,7 @@ export default class DiscordConnection extends Connection { client_secret: this.settings.clientSecret!, grant_type: "authorization_code", code: code, - redirect_uri: `${ - Config.get().cdn.endpointPrivate || - "http://localhost:3001" - }/connections/${this.id}/callback`, + redirect_uri: this.getRedirectUri(), }), ) .post() diff --git a/src/connections/EpicGames/index.ts b/src/connections/EpicGames/index.ts index 247d2435..db09c74f 100644 --- a/src/connections/EpicGames/index.ts +++ b/src/connections/EpicGames/index.ts @@ -1,5 +1,4 @@ import { - Config, ConnectedAccount, ConnectedAccountCommonOAuthTokenResponse, ConnectionCallbackSchema, @@ -47,13 +46,7 @@ export default class EpicGamesConnection extends Connection { const url = new URL(this.authorizeUrl); url.searchParams.append("client_id", this.settings.clientId!); - // TODO: probably shouldn't rely on cdn as this could be different from what we actually want. we should have an api endpoint setting. - url.searchParams.append( - "redirect_uri", - `${ - Config.get().cdn.endpointPrivate || "http://localhost:3001" - }/connections/${this.id}/callback`, - ); + url.searchParams.append("redirect_uri", this.getRedirectUri()); url.searchParams.append("response_type", "code"); url.searchParams.append("scope", this.scopes.join(" ")); url.searchParams.append("state", state); diff --git a/src/connections/Facebook/index.ts b/src/connections/Facebook/index.ts index 5413f867..cc298ed7 100644 --- a/src/connections/Facebook/index.ts +++ b/src/connections/Facebook/index.ts @@ -1,5 +1,4 @@ import { - Config, ConnectedAccount, ConnectedAccountCommonOAuthTokenResponse, ConnectionCallbackSchema, @@ -46,13 +45,7 @@ export default class FacebookConnection extends Connection { const url = new URL(this.authorizeUrl); url.searchParams.append("client_id", this.settings.clientId!); - // TODO: probably shouldn't rely on cdn as this could be different from what we actually want. we should have an api endpoint setting. - url.searchParams.append( - "redirect_uri", - `${ - Config.get().cdn.endpointPrivate || "http://localhost:3001" - }/connections/${this.id}/callback`, - ); + url.searchParams.append("redirect_uri", this.getRedirectUri()); url.searchParams.append("state", state); url.searchParams.append("response_type", "code"); url.searchParams.append("scope", this.scopes.join(" ")); @@ -65,12 +58,7 @@ export default class FacebookConnection extends Connection { url.searchParams.append("client_id", this.settings.clientId!); url.searchParams.append("client_secret", this.settings.clientSecret!); url.searchParams.append("code", code); - url.searchParams.append( - "redirect_uri", - `${ - Config.get().cdn.endpointPrivate || "http://localhost:3001" - }/connections/${this.id}/callback`, - ); + url.searchParams.append("redirect_uri", this.getRedirectUri()); return url.toString(); } diff --git a/src/connections/GitHub/index.ts b/src/connections/GitHub/index.ts index 8380e765..ea5e5493 100644 --- a/src/connections/GitHub/index.ts +++ b/src/connections/GitHub/index.ts @@ -1,5 +1,4 @@ import { - Config, ConnectedAccount, ConnectedAccountCommonOAuthTokenResponse, ConnectionCallbackSchema, @@ -36,13 +35,7 @@ export default class GitHubConnection extends Connection { const url = new URL(this.authorizeUrl); url.searchParams.append("client_id", this.settings.clientId!); - // TODO: probably shouldn't rely on cdn as this could be different from what we actually want. we should have an api endpoint setting. - url.searchParams.append( - "redirect_uri", - `${ - Config.get().cdn.endpointPrivate || "http://localhost:3001" - }/connections/${this.id}/callback`, - ); + url.searchParams.append("redirect_uri", this.getRedirectUri()); url.searchParams.append("scope", this.scopes.join(" ")); url.searchParams.append("state", state); return url.toString(); diff --git a/src/connections/Reddit/index.ts b/src/connections/Reddit/index.ts index 70b4a8af..7e5a1318 100644 --- a/src/connections/Reddit/index.ts +++ b/src/connections/Reddit/index.ts @@ -1,5 +1,4 @@ import { - Config, ConnectedAccount, ConnectedAccountCommonOAuthTokenResponse, ConnectionCallbackSchema, @@ -48,13 +47,7 @@ export default class RedditConnection extends Connection { const url = new URL(this.authorizeUrl); url.searchParams.append("client_id", this.settings.clientId!); - // TODO: probably shouldn't rely on cdn as this could be different from what we actually want. we should have an api endpoint setting. - url.searchParams.append( - "redirect_uri", - `${ - Config.get().cdn.endpointPrivate || "http://localhost:3001" - }/connections/${this.id}/callback`, - ); + url.searchParams.append("redirect_uri", this.getRedirectUri()); url.searchParams.append("response_type", "code"); url.searchParams.append("scope", this.scopes.join(" ")); url.searchParams.append("state", state); @@ -85,10 +78,7 @@ export default class RedditConnection extends Connection { new URLSearchParams({ grant_type: "authorization_code", code: code, - redirect_uri: `${ - Config.get().cdn.endpointPrivate || - "http://localhost:3001" - }/connections/${this.id}/callback`, + redirect_uri: this.getRedirectUri(), }), ) .post() diff --git a/src/connections/Spotify/index.ts b/src/connections/Spotify/index.ts index 54ec2696..ff06d341 100644 --- a/src/connections/Spotify/index.ts +++ b/src/connections/Spotify/index.ts @@ -1,5 +1,4 @@ import { - Config, ConnectedAccount, ConnectedAccountCommonOAuthTokenResponse, ConnectionCallbackSchema, @@ -57,13 +56,7 @@ export default class SpotifyConnection extends RefreshableConnection { const url = new URL(this.authorizeUrl); url.searchParams.append("client_id", this.settings.clientId!); - // TODO: probably shouldn't rely on cdn as this could be different from what we actually want. we should have an api endpoint setting. - url.searchParams.append( - "redirect_uri", - `${ - Config.get().cdn.endpointPrivate || "http://localhost:3001" - }/connections/${this.id}/callback`, - ); + url.searchParams.append("redirect_uri", this.getRedirectUri()); url.searchParams.append("response_type", "code"); url.searchParams.append("scope", this.scopes.join(" ")); url.searchParams.append("state", state); @@ -94,10 +87,7 @@ export default class SpotifyConnection extends RefreshableConnection { new URLSearchParams({ grant_type: "authorization_code", code: code, - redirect_uri: `${ - Config.get().cdn.endpointPrivate || - "http://localhost:3001" - }/connections/${this.id}/callback`, + redirect_uri: this.getRedirectUri(), }), ) .post() diff --git a/src/connections/Twitch/index.ts b/src/connections/Twitch/index.ts index 264db3cc..7cc88caa 100644 --- a/src/connections/Twitch/index.ts +++ b/src/connections/Twitch/index.ts @@ -1,5 +1,4 @@ import { - Config, ConnectedAccount, ConnectedAccountCommonOAuthTokenResponse, ConnectionCallbackSchema, @@ -49,13 +48,7 @@ export default class TwitchConnection extends RefreshableConnection { const url = new URL(this.authorizeUrl); url.searchParams.append("client_id", this.settings.clientId!); - // TODO: probably shouldn't rely on cdn as this could be different from what we actually want. we should have an api endpoint setting. - url.searchParams.append( - "redirect_uri", - `${ - Config.get().cdn.endpointPrivate || "http://localhost:3001" - }/connections/${this.id}/callback`, - ); + url.searchParams.append("redirect_uri", this.getRedirectUri()); url.searchParams.append("response_type", "code"); url.searchParams.append("scope", this.scopes.join(" ")); url.searchParams.append("state", state); @@ -85,10 +78,7 @@ export default class TwitchConnection extends RefreshableConnection { code: code, client_id: this.settings.clientId!, client_secret: this.settings.clientSecret!, - redirect_uri: `${ - Config.get().cdn.endpointPrivate || - "http://localhost:3001" - }/connections/${this.id}/callback`, + redirect_uri: this.getRedirectUri(), }), ) .post() diff --git a/src/connections/Twitter/index.ts b/src/connections/Twitter/index.ts index ad9d55d4..8292b2c5 100644 --- a/src/connections/Twitter/index.ts +++ b/src/connections/Twitter/index.ts @@ -1,5 +1,4 @@ import { - Config, ConnectedAccount, ConnectedAccountCommonOAuthTokenResponse, ConnectionCallbackSchema, @@ -49,13 +48,7 @@ export default class TwitterConnection extends RefreshableConnection { const url = new URL(this.authorizeUrl); url.searchParams.append("client_id", this.settings.clientId!); - // TODO: probably shouldn't rely on cdn as this could be different from what we actually want. we should have an api endpoint setting. - url.searchParams.append( - "redirect_uri", - `${ - Config.get().cdn.endpointPrivate || "http://localhost:3001" - }/connections/${this.id}/callback`, - ); + url.searchParams.append("redirect_uri", this.getRedirectUri()); url.searchParams.append("response_type", "code"); url.searchParams.append("scope", this.scopes.join(" ")); url.searchParams.append("state", state); @@ -89,10 +82,7 @@ export default class TwitterConnection extends RefreshableConnection { grant_type: "authorization_code", code: code, client_id: this.settings.clientId!, - redirect_uri: `${ - Config.get().cdn.endpointPrivate || - "http://localhost:3001" - }/connections/${this.id}/callback`, + redirect_uri: this.getRedirectUri(), code_verifier: "challenge", // TODO: properly use PKCE challenge }), ) @@ -126,10 +116,7 @@ export default class TwitterConnection extends RefreshableConnection { grant_type: "refresh_token", refresh_token, client_id: this.settings.clientId!, - redirect_uri: `${ - Config.get().cdn.endpointPrivate || - "http://localhost:3001" - }/connections/${this.id}/callback`, + redirect_uri: this.getRedirectUri(), code_verifier: "challenge", // TODO: properly use PKCE challenge }), ) diff --git a/src/connections/Xbox/index.ts b/src/connections/Xbox/index.ts index 80a04dea..1f736373 100644 --- a/src/connections/Xbox/index.ts +++ b/src/connections/Xbox/index.ts @@ -1,5 +1,4 @@ import { - Config, ConnectedAccount, ConnectedAccountCommonOAuthTokenResponse, ConnectionCallbackSchema, @@ -56,13 +55,7 @@ export default class XboxConnection extends Connection { const url = new URL(this.authorizeUrl); url.searchParams.append("client_id", this.settings.clientId!); - // TODO: probably shouldn't rely on cdn as this could be different from what we actually want. we should have an api endpoint setting. - url.searchParams.append( - "redirect_uri", - `${ - Config.get().cdn.endpointPrivate || "http://localhost:3001" - }/connections/${this.id}/callback`, - ); + url.searchParams.append("redirect_uri", this.getRedirectUri()); url.searchParams.append("response_type", "code"); url.searchParams.append("scope", this.scopes.join(" ")); url.searchParams.append("state", state); @@ -121,10 +114,7 @@ export default class XboxConnection extends Connection { grant_type: "authorization_code", code: code, client_id: this.settings.clientId!, - redirect_uri: `${ - Config.get().cdn.endpointPrivate || - "http://localhost:3001" - }/connections/${this.id}/callback`, + redirect_uri: this.getRedirectUri(), scope: this.scopes.join(" "), }), ) diff --git a/src/connections/Youtube/index.ts b/src/connections/Youtube/index.ts index afc9356b..9fa8eb38 100644 --- a/src/connections/Youtube/index.ts +++ b/src/connections/Youtube/index.ts @@ -1,5 +1,4 @@ import { - Config, ConnectedAccount, ConnectedAccountCommonOAuthTokenResponse, ConnectionCallbackSchema, @@ -56,13 +55,7 @@ export default class YoutubeConnection extends Connection { const url = new URL(this.authorizeUrl); url.searchParams.append("client_id", this.settings.clientId!); - // TODO: probably shouldn't rely on cdn as this could be different from what we actually want. we should have an api endpoint setting. - url.searchParams.append( - "redirect_uri", - `${ - Config.get().cdn.endpointPrivate || "http://localhost:3001" - }/connections/${this.id}/callback`, - ); + url.searchParams.append("redirect_uri", this.getRedirectUri()); url.searchParams.append("response_type", "code"); url.searchParams.append("scope", this.scopes.join(" ")); url.searchParams.append("state", state); @@ -92,10 +85,7 @@ export default class YoutubeConnection extends Connection { code: code, client_id: this.settings.clientId!, client_secret: this.settings.clientSecret!, - redirect_uri: `${ - Config.get().cdn.endpointPrivate || - "http://localhost:3001" - }/connections/${this.id}/callback`, + redirect_uri: this.getRedirectUri(), }), ) .post() diff --git a/src/util/config/types/ApiConfiguration.ts b/src/util/config/types/ApiConfiguration.ts index 0389ed3e..579b1f2d 100644 --- a/src/util/config/types/ApiConfiguration.ts +++ b/src/util/config/types/ApiConfiguration.ts @@ -20,5 +20,5 @@ export class ApiConfiguration { defaultVersion: string = "9"; activeVersions: string[] = ["6", "7", "8", "9"]; useFosscordEnhancements: boolean = true; - endpointPublic: string = "/api"; + endpointPublic: string | null = null; } diff --git a/src/util/connections/Connection.ts b/src/util/connections/Connection.ts index 8b60b0d2..26279299 100644 --- a/src/util/connections/Connection.ts +++ b/src/util/connections/Connection.ts @@ -1,7 +1,7 @@ import crypto from "crypto"; import { ConnectedAccount } from "../entities"; import { ConnectedAccountSchema, ConnectionCallbackSchema } from "../schemas"; -import { DiscordApiErrors } from "../util"; +import { Config, DiscordApiErrors } from "../util"; /** * A connection that can be used to connect to an external service. @@ -19,6 +19,16 @@ export default abstract class Connection { */ abstract getAuthorizationUrl(userId: string): string; + /** + * Returns the redirect_uri for a connection type + * @returns redirect_uri for this connection + */ + getRedirectUri() { + const endpointPublic = + Config.get().api.endpointPublic ?? "http://localhost:3001"; + return `${endpointPublic}/connections/${this.id}/callback`; + } + /** * Processes the callback * @param args Callback arguments -- cgit 1.5.1