feat: add nextcloud talk app (#16455)
* Initial draft of nextcloud app * Add video adapter for creating a new conversation via API * Make links dynamic, add nc user and pwd for auth, fix link creation * Add nextcloudTalk app to yarn.lock * Fix screenshot name * Add OAuth2 handler * Fix type for booking reference and fix access token header param, (for creating and deleting a meeting) * Remove comment * Add README.md file * Fix redirect URL for authenticating Nextcloud user --------- Co-authored-by: Amit Sharma <74371312+Amit91848@users.noreply.github.com> Co-authored-by: Anik Dhabal Babu <81948346+anikdhabal@users.noreply.github.com>
This commit is contained in:
co-authored by
Amit Sharma
Anik Dhabal Babu
parent
1a4cf5f91d
commit
de503add83
@@ -23,6 +23,7 @@ import { appKeysSchema as make_zod_ts } from "./make/zod";
|
||||
import { appKeysSchema as matomo_zod_ts } from "./matomo/zod";
|
||||
import { appKeysSchema as metapixel_zod_ts } from "./metapixel/zod";
|
||||
import { appKeysSchema as mock_payment_app_zod_ts } from "./mock-payment-app/zod";
|
||||
import { appKeysSchema as nextcloudtalk_zod_ts } from "./nextcloudtalk/zod";
|
||||
import { appKeysSchema as office365calendar_zod_ts } from "./office365calendar/zod";
|
||||
import { appKeysSchema as office365video_zod_ts } from "./office365video/zod";
|
||||
import { appKeysSchema as paypal_zod_ts } from "./paypal/zod";
|
||||
@@ -70,6 +71,7 @@ export const appKeysSchemas = {
|
||||
matomo: matomo_zod_ts,
|
||||
metapixel: metapixel_zod_ts,
|
||||
"mock-payment-app": mock_payment_app_zod_ts,
|
||||
nextcloudtalk: nextcloudtalk_zod_ts,
|
||||
office365calendar: office365calendar_zod_ts,
|
||||
office365video: office365video_zod_ts,
|
||||
paypal: paypal_zod_ts,
|
||||
|
||||
@@ -48,6 +48,7 @@ import metapixel_config_json from "./metapixel/config.json";
|
||||
import mirotalk_config_json from "./mirotalk/config.json";
|
||||
import mock_payment_app_config_json from "./mock-payment-app/config.json";
|
||||
import n8n_config_json from "./n8n/config.json";
|
||||
import nextcloudtalk_config_json from "./nextcloudtalk/config.json";
|
||||
import { metadata as office365calendar__metadata_ts } from "./office365calendar/_metadata";
|
||||
import office365video_config_json from "./office365video/config.json";
|
||||
import paypal_config_json from "./paypal/config.json";
|
||||
@@ -144,6 +145,7 @@ export const appStoreMetadata = {
|
||||
mirotalk: mirotalk_config_json,
|
||||
"mock-payment-app": mock_payment_app_config_json,
|
||||
n8n: n8n_config_json,
|
||||
nextcloudtalk: nextcloudtalk_config_json,
|
||||
office365calendar: office365calendar__metadata_ts,
|
||||
office365video: office365video_config_json,
|
||||
paypal: paypal_config_json,
|
||||
|
||||
@@ -23,6 +23,7 @@ import { appDataSchema as make_zod_ts } from "./make/zod";
|
||||
import { appDataSchema as matomo_zod_ts } from "./matomo/zod";
|
||||
import { appDataSchema as metapixel_zod_ts } from "./metapixel/zod";
|
||||
import { appDataSchema as mock_payment_app_zod_ts } from "./mock-payment-app/zod";
|
||||
import { appDataSchema as nextcloudtalk_zod_ts } from "./nextcloudtalk/zod";
|
||||
import { appDataSchema as office365calendar_zod_ts } from "./office365calendar/zod";
|
||||
import { appDataSchema as office365video_zod_ts } from "./office365video/zod";
|
||||
import { appDataSchema as paypal_zod_ts } from "./paypal/zod";
|
||||
@@ -70,6 +71,7 @@ export const appDataSchemas = {
|
||||
matomo: matomo_zod_ts,
|
||||
metapixel: metapixel_zod_ts,
|
||||
"mock-payment-app": mock_payment_app_zod_ts,
|
||||
nextcloudtalk: nextcloudtalk_zod_ts,
|
||||
office365calendar: office365calendar_zod_ts,
|
||||
office365video: office365video_zod_ts,
|
||||
paypal: paypal_zod_ts,
|
||||
|
||||
@@ -48,6 +48,7 @@ export const apiHandlers = {
|
||||
mirotalk: import("./mirotalk/api"),
|
||||
"mock-payment-app": import("./mock-payment-app/api"),
|
||||
n8n: import("./n8n/api"),
|
||||
nextcloudtalk: import("./nextcloudtalk/api"),
|
||||
office365calendar: import("./office365calendar/api"),
|
||||
office365video: import("./office365video/api"),
|
||||
paypal: import("./paypal/api"),
|
||||
|
||||
@@ -22,6 +22,7 @@ import { metadata as jitsivideo__metadata_ts } from "./jitsivideo/_metadata";
|
||||
import matomo_config_json from "./matomo/config.json";
|
||||
import metapixel_config_json from "./metapixel/config.json";
|
||||
import mirotalk_config_json from "./mirotalk/config.json";
|
||||
import nextcloudtalk_config_json from "./nextcloudtalk/config.json";
|
||||
import office365video_config_json from "./office365video/config.json";
|
||||
import ping_config_json from "./ping/config.json";
|
||||
import plausible_config_json from "./plausible/config.json";
|
||||
@@ -66,6 +67,7 @@ export const appStoreMetadata = {
|
||||
matomo: matomo_config_json,
|
||||
metapixel: metapixel_config_json,
|
||||
mirotalk: mirotalk_config_json,
|
||||
nextcloudtalk: nextcloudtalk_config_json,
|
||||
office365video: office365video_config_json,
|
||||
ping: ping_config_json,
|
||||
plausible: plausible_config_json,
|
||||
|
||||
@@ -15,6 +15,7 @@ const appStore = {
|
||||
jellyconferencing: () => import("./jelly"),
|
||||
jitsivideo: () => import("./jitsivideo"),
|
||||
larkcalendar: () => import("./larkcalendar"),
|
||||
nextcloudtalkvideo: () => import("./nextcloudtalk"),
|
||||
office365calendar: () => import("./office365calendar"),
|
||||
office365video: () => import("./office365video"),
|
||||
plausible: () => import("./plausible"),
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
---
|
||||
items:
|
||||
- nextcloudtalk1.jpg
|
||||
- nextcloudtalk2.jpg
|
||||
---
|
||||
|
||||
{DESCRIPTION}
|
||||
@@ -0,0 +1,11 @@
|
||||
### Obtaining Nextcloud Client ID and Secret
|
||||
|
||||
1. Create a [Nextcloud account](https://nextcloud.com/sign-up), if you don't already have one or [self-host Nextcloud](https://nextcloud.com/install/#instructions-server).
|
||||
2. Sign in as admin on your Nextcloud.
|
||||
3. On the upper right, click the profile icon and go to "Administration settings".
|
||||
4. In the left panel under "Administration" click on "Security".
|
||||
5. Scroll down until the end of the page to "OAuth 2.0 clients".
|
||||
6. Under "Add client" give your client a name, e.g. "Cal.com".
|
||||
7. Set the Redirection URI as `<Cal.com URL>/api/integrations/nextcloudtalk/callback` replacing Cal.com URL with the URI at which your application runs.
|
||||
8. Click "Add".
|
||||
9. Copy the Client Identifier and Secret key and add these while enabling the app through Settings -> Admin -> Apps interface
|
||||
@@ -0,0 +1,36 @@
|
||||
import type { NextApiRequest } from "next";
|
||||
import { stringify } from "querystring";
|
||||
|
||||
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import { defaultHandler, defaultResponder } from "@calcom/lib/server";
|
||||
|
||||
import getAppKeysFromSlug from "../../_utils/getAppKeysFromSlug";
|
||||
import { encodeOAuthState } from "../../_utils/oauth/encodeOAuthState";
|
||||
|
||||
async function handler(req: NextApiRequest) {
|
||||
const user = req?.session?.user;
|
||||
if (!user) {
|
||||
return { status: 401, body: { error: "Unauthorized" } };
|
||||
}
|
||||
|
||||
const appKeys = await getAppKeysFromSlug("nextcloudtalk");
|
||||
const hostUrl = appKeys.nextcloudTalkHost as string;
|
||||
const client_id = appKeys.nextcloudTalkClientId as string;
|
||||
const client_secret = appKeys.nextcloudTalkClientSecret as string;
|
||||
const state = encodeOAuthState(req);
|
||||
|
||||
const params = {
|
||||
response_type: "code",
|
||||
client_id,
|
||||
client_secret,
|
||||
redirect_uri: `${WEBAPP_URL}/api/integrations/nextcloudtalk/callback`,
|
||||
state,
|
||||
};
|
||||
const query = stringify(params);
|
||||
const url = `${hostUrl}/index.php/apps/oauth2/authorize?${query}`;
|
||||
return { url };
|
||||
}
|
||||
|
||||
export default defaultHandler({
|
||||
GET: Promise.resolve({ default: defaultResponder(handler) }),
|
||||
});
|
||||
@@ -0,0 +1,70 @@
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import { getSafeRedirectUrl } from "@calcom/lib/getSafeRedirectUrl";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
import getAppKeysFromSlug from "../../_utils/getAppKeysFromSlug";
|
||||
import getInstalledAppPath from "../../_utils/getInstalledAppPath";
|
||||
import createOAuthAppCredential from "../../_utils/oauth/createOAuthAppCredential";
|
||||
import { decodeOAuthState } from "../../_utils/oauth/decodeOAuthState";
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
const state = decodeOAuthState(req);
|
||||
const userId = req.session?.user.id;
|
||||
if (!userId) {
|
||||
return res.status(404).json({ message: "No user found" });
|
||||
}
|
||||
const { code } = req.query;
|
||||
const appKeys = await getAppKeysFromSlug("nextcloudtalk");
|
||||
const hostUrl = appKeys.nextcloudTalkHost as string;
|
||||
const client_id = appKeys.nextcloudTalkClientId as string;
|
||||
const client_secret = appKeys.nextcloudTalkClientSecret as string;
|
||||
|
||||
const result = await fetch(`${hostUrl}/index.php/apps/oauth2/api/v1/token`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
grant_type: "authorization_code",
|
||||
code,
|
||||
client_id,
|
||||
client_secret,
|
||||
redirect_uri: `${WEBAPP_URL}/api/integrations/nextcloudtalk/callback`,
|
||||
}),
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
|
||||
if (result.status !== 200) {
|
||||
let errorMessage = "Something is wrong with the Nextcloud API";
|
||||
try {
|
||||
const responseBody = await result.json();
|
||||
errorMessage = responseBody.error;
|
||||
} catch (e) {}
|
||||
|
||||
res.status(400).json({ message: errorMessage });
|
||||
return;
|
||||
}
|
||||
|
||||
const responseBody = await result.json();
|
||||
if (responseBody.error) {
|
||||
res.status(400).json({ message: responseBody.error });
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove the previous credential if there were linked ones already
|
||||
await prisma.credential.deleteMany({
|
||||
where: {
|
||||
type: "nextcloudtalk_video",
|
||||
userId,
|
||||
appId: "nextcloudtalk",
|
||||
},
|
||||
});
|
||||
|
||||
await createOAuthAppCredential({ appId: "nextcloudtalk", type: "nextcloudtalk_video" }, responseBody, req);
|
||||
|
||||
res.redirect(
|
||||
getSafeRedirectUrl(state?.returnTo) ??
|
||||
getInstalledAppPath({ variant: "conferencing", slug: "nextcloudtalk" })
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export { default as add } from "./add";
|
||||
export { default as callback } from "./callback";
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"/*": "Don't modify slug - If required, do it using cli edit command",
|
||||
"name": "Nextcloud Talk",
|
||||
"slug": "nextcloudtalk",
|
||||
"type": "nextcloudtalk_video",
|
||||
"logo": "icon.svg",
|
||||
"url": "https://nextcloud.com/talk",
|
||||
"variant": "conferencing",
|
||||
"categories": ["conferencing"],
|
||||
"publisher": "Cal.com",
|
||||
"email": "help@cal.com",
|
||||
"appData": {
|
||||
"location": {
|
||||
"type": "integrations:{SLUG}_conferencing",
|
||||
"label": "{TITLE}",
|
||||
"linkType": "dynamic"
|
||||
}
|
||||
},
|
||||
"description": "Nextcloud Talk is a fully self hosted, on-premises audio/video and chat communication service. It features web and mobile apps and is designed to offer the highest degree of security while being easy to use.",
|
||||
"isTemplate": false,
|
||||
"__createdUsingCli": true
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
export * as api from "./api";
|
||||
export * as lib from "./lib";
|
||||
@@ -0,0 +1,209 @@
|
||||
import { stringify } from "querystring";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import { z } from "zod";
|
||||
|
||||
import logger from "@calcom/lib/logger";
|
||||
import { safeStringify } from "@calcom/lib/safeStringify";
|
||||
import prisma from "@calcom/prisma";
|
||||
import type { CalendarEvent } from "@calcom/types/Calendar";
|
||||
import type { CredentialPayload } from "@calcom/types/Credential";
|
||||
import type { PartialReference } from "@calcom/types/EventManager";
|
||||
import type { VideoApiAdapter, VideoCallData } from "@calcom/types/VideoApiAdapter";
|
||||
|
||||
import getAppKeysFromSlug from "../../_utils/getAppKeysFromSlug";
|
||||
import getParsedAppKeysFromSlug from "../../_utils/getParsedAppKeysFromSlug";
|
||||
import { invalidateCredential } from "../../_utils/invalidateCredential";
|
||||
import { OAuthManager } from "../../_utils/oauth/OAuthManager";
|
||||
import { markTokenAsExpired } from "../../_utils/oauth/markTokenAsExpired";
|
||||
import { oAuthManagerHelper } from "../../_utils/oauth/oAuthManagerHelper";
|
||||
import config from "../config.json";
|
||||
import { appKeysSchema } from "../zod";
|
||||
|
||||
const nextcloudEventResultSchema = z.object({
|
||||
ocs: z.object({
|
||||
meta: z.object({
|
||||
status: z.string(),
|
||||
statuscode: z.number(),
|
||||
message: z.string(),
|
||||
}),
|
||||
data: z.object({
|
||||
token: z.string(),
|
||||
}),
|
||||
}),
|
||||
});
|
||||
|
||||
const NextcloudTalkVideoApiAdapter = (credential: CredentialPayload): VideoApiAdapter => {
|
||||
const tokenResponse = oAuthManagerHelper.getTokenObjectFromCredential(credential);
|
||||
|
||||
const clientCredentials = getParsedAppKeysFromSlug(config.slug, appKeysSchema);
|
||||
|
||||
const fetchNextcloudApi = async (endpoint: string, options?: RequestInit) => {
|
||||
const auth = new OAuthManager({
|
||||
credentialSyncVariables: oAuthManagerHelper.credentialSyncVariables,
|
||||
resourceOwner: {
|
||||
type: "user",
|
||||
id: credential.userId,
|
||||
},
|
||||
appSlug: config.slug,
|
||||
currentTokenObject: tokenResponse,
|
||||
fetchNewTokenObject: async ({ refreshToken }: { refreshToken: string | null }) => {
|
||||
const { nextcloudTalkClientId, nextcloudTalkClientSecret, nextcloudTalkHost } =
|
||||
await clientCredentials;
|
||||
if (!refreshToken) {
|
||||
return null;
|
||||
}
|
||||
const params = {
|
||||
client_id: nextcloudTalkClientId,
|
||||
client_secret: nextcloudTalkClientSecret,
|
||||
refresh_token: refreshToken,
|
||||
grant_type: "refresh_token",
|
||||
};
|
||||
const query = stringify(params);
|
||||
return fetch(`${nextcloudTalkHost}/index.php/apps/oauth2/api/v1/token?${query}`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: new URLSearchParams({}),
|
||||
});
|
||||
},
|
||||
isTokenObjectUnusable: async function (response) {
|
||||
const myLog = logger.getSubLogger({ prefix: ["nextcloudtalkvideo:isTokenObjectUnusable"] });
|
||||
myLog.debug(safeStringify({ status: response.status, ok: response.ok }));
|
||||
if (!response.ok || (response.status < 200 && response.status >= 300)) {
|
||||
const responseBody = await response.json();
|
||||
myLog.debug(safeStringify({ responseBody }));
|
||||
|
||||
if (responseBody.error === "invalid_grant") {
|
||||
return { reason: responseBody.error };
|
||||
}
|
||||
}
|
||||
return null;
|
||||
},
|
||||
isAccessTokenUnusable: async function (response) {
|
||||
const myLog = logger.getSubLogger({ prefix: ["nextcloudtalkvideo:isAccessTokenUnusable"] });
|
||||
myLog.debug(safeStringify({ status: response.status, ok: response.ok }));
|
||||
if (!response.ok || (response.status < 200 && response.status >= 300)) {
|
||||
const responseBody = await response.json();
|
||||
myLog.debug(safeStringify({ responseBody }));
|
||||
|
||||
if (responseBody.code === 124) {
|
||||
return { reason: responseBody.message ?? "" };
|
||||
}
|
||||
}
|
||||
return null;
|
||||
},
|
||||
invalidateTokenObject: () => invalidateCredential(credential.id),
|
||||
expireAccessToken: () => markTokenAsExpired(credential),
|
||||
updateTokenObject: async (newTokenObject) => {
|
||||
await prisma.credential.update({
|
||||
where: {
|
||||
id: credential.id,
|
||||
},
|
||||
data: {
|
||||
key: newTokenObject,
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const { nextcloudTalkHost } = await clientCredentials;
|
||||
const { json } = await auth.request({
|
||||
url: `${nextcloudTalkHost}/${endpoint}`,
|
||||
options: {
|
||||
method: "GET",
|
||||
...options,
|
||||
headers: {
|
||||
...options?.headers,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return json;
|
||||
};
|
||||
|
||||
return {
|
||||
getAvailability: () => {
|
||||
return Promise.resolve([]);
|
||||
},
|
||||
createMeeting: async (eventData: CalendarEvent): Promise<VideoCallData> => {
|
||||
const appKeys = await getAppKeysFromSlug(config.slug);
|
||||
const meetingPattern = (appKeys.nextcloudTalkPattern as string) || "{uuid}";
|
||||
const hostUrl = appKeys.nextcloudTalkHost as string;
|
||||
|
||||
//Allows "/{Type}-with-{Attendees}" slug
|
||||
const meetingID = meetingPattern
|
||||
.replaceAll("{uuid}", uuidv4())
|
||||
.replaceAll("{Title}", eventData.title)
|
||||
.replaceAll("{Event Type Title}", eventData.type)
|
||||
.replaceAll("{Scheduler}", eventData.attendees.map((a) => a.name).join("-"))
|
||||
.replaceAll("{Organizer}", eventData.organizer.name)
|
||||
.replaceAll("{Location}", eventData.location || "")
|
||||
.replaceAll("{Team}", eventData.team?.name || "")
|
||||
.replaceAll(" ", "-"); //Last Rule! - Replace all blanks (%20) with dashes;
|
||||
|
||||
try {
|
||||
// Create video link with room type 3 (constant for a public room, see https://nextcloud-talk.readthedocs.io/en/stable/constants/#conversation-types)
|
||||
const response = await fetchNextcloudApi(`ocs/v2.php/apps/spreed/api/v4/room`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json",
|
||||
"OCS-APIRequest": "true",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
roomType: 3,
|
||||
roomName: `${meetingID}`,
|
||||
}),
|
||||
});
|
||||
const result = nextcloudEventResultSchema.parse(response);
|
||||
|
||||
if (result.ocs && result.ocs.data) {
|
||||
return {
|
||||
type: config.type,
|
||||
id: result.ocs.data.token,
|
||||
password: "",
|
||||
url: `${hostUrl}/call/${result.ocs.data.token}`,
|
||||
};
|
||||
}
|
||||
throw new Error(`Failed to create meeting. Response is ${JSON.stringify(result)}`);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
/* Prevents meeting creation failure when token is expired */
|
||||
throw new Error("Unexpected error");
|
||||
}
|
||||
},
|
||||
deleteMeeting: async (uid: string): Promise<void> => {
|
||||
try {
|
||||
// Remove video link from Nextcloud
|
||||
const response = await fetchNextcloudApi(`ocs/v2.php/apps/spreed/api/v4/room/${uid}`, {
|
||||
method: "DELETE",
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
"Content-Type": "application/json",
|
||||
"OCS-APIRequest": "true",
|
||||
},
|
||||
});
|
||||
const result = nextcloudEventResultSchema.parse(response);
|
||||
|
||||
if (result.ocs && result.ocs.meta && result.ocs.meta.status === "ok") {
|
||||
return Promise.resolve();
|
||||
}
|
||||
throw new Error(`Failed to delete meeting. Response is ${JSON.stringify(result)}`);
|
||||
} catch (err) {
|
||||
return Promise.reject(new Error("Failed to delete meeting"));
|
||||
}
|
||||
},
|
||||
updateMeeting: (bookingRef: PartialReference): Promise<VideoCallData> => {
|
||||
return Promise.resolve({
|
||||
type: config.type,
|
||||
id: bookingRef.meetingId as string,
|
||||
password: bookingRef.meetingPassword as string,
|
||||
url: bookingRef.meetingUrl as string,
|
||||
});
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export default NextcloudTalkVideoApiAdapter;
|
||||
@@ -0,0 +1 @@
|
||||
export { default as VideoApiAdapter } from "./VideoApiAdapter";
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/package.json",
|
||||
"private": true,
|
||||
"name": "@calcom/nextcloudtalk",
|
||||
"version": "0.0.0",
|
||||
"main": "./index.ts",
|
||||
"dependencies": {
|
||||
"@calcom/lib": "*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@calcom/types": "*"
|
||||
},
|
||||
"description": "Nextcloud Talk is a fully self hosted, on-premises audio/video and chat communication service. It features web and mobile apps and is designed to offer the highest degree of security while being easy to use."
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
inkscape:version="1.1-dev (f9311a1, 2019-12-25)"
|
||||
sodipodi:docname="talk8.svg"
|
||||
id="svg19"
|
||||
xml:space="preserve"
|
||||
viewBox="0 0 1024 1024"
|
||||
version="1.1"
|
||||
stroke-miterlimit="1.4142"
|
||||
stroke-linejoin="round"
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"><metadata
|
||||
id="metadata23"><rdf:RDF><cc:Work
|
||||
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><sodipodi:namedview
|
||||
inkscape:current-layer="svg19"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:window-y="23"
|
||||
inkscape:window-x="1440"
|
||||
inkscape:cy="522.40348"
|
||||
inkscape:cx="510.51379"
|
||||
inkscape:zoom="0.67285156"
|
||||
showgrid="false"
|
||||
id="namedview21"
|
||||
inkscape:window-height="1035"
|
||||
inkscape:window-width="1920"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:pageopacity="0"
|
||||
guidetolerance="10"
|
||||
gridtolerance="10"
|
||||
objecttolerance="10"
|
||||
borderopacity="1"
|
||||
inkscape:document-rotation="0"
|
||||
bordercolor="#666666"
|
||||
pagecolor="#ffffff" /><defs
|
||||
id="defs15"><linearGradient
|
||||
gradientUnits="userSpaceOnUse"
|
||||
gradientTransform="matrix(8.96 0 0 8.96 -7.8457e-5 .00019795)"
|
||||
y2="-7.6294e-6"
|
||||
y1="150"
|
||||
x2="150"
|
||||
x1="18.23"
|
||||
id="a"><stop
|
||||
id="stop10"
|
||||
offset="0"
|
||||
stop-color="#0082c9" /><stop
|
||||
id="stop12"
|
||||
offset="1"
|
||||
stop-color="#1cafff" /></linearGradient></defs>
|
||||
<rect
|
||||
id="rect17"
|
||||
fill-rule="evenodd"
|
||||
fill="url(#a)"
|
||||
height="1024"
|
||||
width="1024"
|
||||
rx="160" /><path
|
||||
style="fill:#ffffff"
|
||||
inkscape:connector-curvature="0"
|
||||
d="M 511.95919,186 A 325.96385,325.95103 0 0 0 186,511.96034 325.96385,325.95103 0 0 0 511.95919,837.91133 325.96385,325.95103 0 0 0 681.04889,790.22529 c 40.06218,15.91895 129.79781,63.14682 151.15526,42.74701 22.3177,-21.31206 -26.20129,-121.61808 -37.83331,-158.89148 A 325.96385,325.95103 0 0 0 837.91466,511.95755 325.96385,325.95103 0 0 0 511.96013,186.01118 Z m 0.0373,123.92323 A 202.1178,202.11161 0 0 1 714.11425,512.03485 202.1178,202.11161 0 0 1 511.99645,714.13247 202.1178,202.11161 0 0 1 309.87866,512.03485 202.1178,202.11161 0 0 1 511.99645,309.92323 Z"
|
||||
stroke-width="0.14"
|
||||
fill="#000"
|
||||
id="path25" /></svg>
|
||||
|
After Width: | Height: | Size: 2.6 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 194 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 334 KiB |
@@ -0,0 +1,10 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export const appKeysSchema = z.object({
|
||||
nextcloudTalkHost: z.string(),
|
||||
nextcloudTalkPattern: z.string().optional(),
|
||||
nextcloudTalkClientId: z.string(),
|
||||
nextcloudTalkClientSecret: z.string(),
|
||||
});
|
||||
|
||||
export const appDataSchema = z.object({});
|
||||
@@ -5051,6 +5051,15 @@ __metadata:
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
"@calcom/nextcloudtalk@workspace:packages/app-store/nextcloudtalk":
|
||||
version: 0.0.0-use.local
|
||||
resolution: "@calcom/nextcloudtalk@workspace:packages/app-store/nextcloudtalk"
|
||||
dependencies:
|
||||
"@calcom/lib": "*"
|
||||
"@calcom/types": "*"
|
||||
languageName: unknown
|
||||
linkType: soft
|
||||
|
||||
"@calcom/office365calendar@workspace:packages/app-store/office365calendar":
|
||||
version: 0.0.0-use.local
|
||||
resolution: "@calcom/office365calendar@workspace:packages/app-store/office365calendar"
|
||||
|
||||
Reference in New Issue
Block a user