Node.js এবং Cloud Run-এর সাহায্যে একটি Google Workspace অ্যাড-অন তৈরি করুন

1। পরিচিতি

Google Workspace অ্যাড-অনগুলি হল কাস্টমাইজ করা অ্যাপ্লিকেশন যা Gmail, ডক্স, শীট এবং স্লাইডের মতো Google Workspace অ্যাপ্লিকেশনগুলির সাথে একত্রিত হয়। তারা ডেভেলপারদের কাস্টমাইজড ইউজার ইন্টারফেস তৈরি করতে সক্ষম করে যা সরাসরি Google Workspace-এ একত্রিত হয়। অ্যাড-অনগুলি ব্যবহারকারীদের কম প্রসঙ্গ স্যুইচিং সহ আরও দক্ষতার সাথে কাজ করতে সহায়তা করে।

এই কোডল্যাবে, আপনি শিখবেন কিভাবে Node.js, Cloud Run এবং Datastore ব্যবহার করে একটি সাধারণ টাস্ক লিস্ট অ্যাড-অন তৈরি এবং স্থাপন করতে হয়।

আপনি কি শিখবেন

  • ক্লাউড শেল ব্যবহার করুন
  • ক্লাউড রানে স্থাপন করুন
  • একটি অ্যাড-অন ডিপ্লয়মেন্ট বর্ণনাকারী তৈরি করুন এবং স্থাপন করুন
  • কার্ড ফ্রেমওয়ার্ক দিয়ে অ্যাড-অন UI তৈরি করুন
  • ব্যবহারকারীর মিথস্ক্রিয়ায় সাড়া দিন
  • একটি অ্যাড-অনে ব্যবহারকারীর প্রসঙ্গকে লিভারেজ করুন

2. সেটআপ এবং প্রয়োজনীয়তা

একটি Google ক্লাউড প্রকল্প তৈরি করতে সেটআপ নির্দেশাবলী অনুসরণ করুন এবং অ্যাড-অন ব্যবহার করা API এবং পরিষেবাগুলি সক্ষম করুন৷

স্ব-গতিসম্পন্ন পরিবেশ সেটআপ

  1. ক্লাউড কনসোল খুলুন এবং একটি নতুন প্রকল্প তৈরি করুন। (আপনার যদি ইতিমধ্যেই একটি Gmail বা Google Workspace অ্যাকাউন্ট না থাকে, তাহলে একটি তৈরি করুন ।)

একটি প্রকল্প মেনু নির্বাচন করুন

নতুন প্রকল্প বোতাম

প্রকল্প আইডি

প্রজেক্ট আইডিটি মনে রাখবেন, সমস্ত Google ক্লাউড প্রকল্প জুড়ে একটি অনন্য নাম (উপরের নামটি ইতিমধ্যে নেওয়া হয়েছে এবং আপনার জন্য কাজ করবে না, দুঃখিত!)। এটি পরে এই কোডল্যাবে PROJECT_ID হিসাবে উল্লেখ করা হবে।

  1. এর পরে, Google ক্লাউড সংস্থানগুলি ব্যবহার করার জন্য, ক্লাউড কনসোলে বিলিং সক্ষম করুন ..

এই কোডল্যাবের মাধ্যমে চালানোর জন্য খুব বেশি খরচ করা উচিত নয়, যদি কিছু থাকে। কোডল্যাবের শেষে "ক্লিন আপ" বিভাগে যে কোনও নির্দেশাবলী অনুসরণ করতে ভুলবেন না যা আপনাকে কীভাবে সংস্থানগুলি বন্ধ করতে হবে তা পরামর্শ দেয় যাতে আপনি এই টিউটোরিয়ালের বাইরে বিলিং করতে না পারেন৷ Google ক্লাউডের নতুন ব্যবহারকারীরা $300USD ফ্রি ট্রায়াল প্রোগ্রামের জন্য যোগ্য৷

গুগল ক্লাউড শেল

যদিও Google ক্লাউড আপনার ল্যাপটপ থেকে দূরবর্তীভাবে পরিচালিত হতে পারে, এই কোডল্যাবে আমরা Google ক্লাউড শেল ব্যবহার করব, ক্লাউডে চলমান একটি কমান্ড লাইন পরিবেশ।

ক্লাউড শেল সক্রিয় করুন

  1. ক্লাউড কনসোল থেকে, ক্লাউড শেল সক্রিয় করুন ক্লিক করুন ক্লাউড শেল আইকন .

মেনু বারে ক্লাউড শেল আইকন

আপনি যখন প্রথমবার ক্লাউড শেল খুলবেন, আপনাকে একটি বর্ণনামূলক স্বাগত বার্তা উপস্থাপন করা হবে। আপনি যদি স্বাগত বার্তাটি দেখতে পান তবে চালিয়ে যান ক্লিক করুন। স্বাগত বার্তা আবার প্রদর্শিত হয় না. এখানে স্বাগত বার্তা:

মেঘ শেল স্বাগত বার্তা

ক্লাউড শেলের সাথে সংযোগ করতে এবং সংযোগ করতে এটির মাত্র কয়েক মুহূর্ত লাগবে৷ সংযোগ করার পরে, আপনি ক্লাউড শেল টার্মিনাল দেখতে পাবেন:

ক্লাউড শেল টার্মিনাল

এই ভার্চুয়াল মেশিনটি আপনার প্রয়োজনীয় সমস্ত ডেভেলপমেন্ট টুল দিয়ে লোড করা হয়েছে। এটি একটি ক্রমাগত 5GB হোম ডিরেক্টরি অফার করে এবং Google ক্লাউডে চলে, যা নেটওয়ার্ক কর্মক্ষমতা এবং প্রমাণীকরণকে ব্যাপকভাবে উন্নত করে। এই কোডল্যাবে আপনার সমস্ত কাজ একটি ব্রাউজার বা আপনার Chromebook দিয়ে করা যেতে পারে।

একবার ক্লাউড শেলের সাথে সংযুক্ত হয়ে গেলে, আপনি দেখতে পাবেন যে আপনি ইতিমধ্যেই প্রমাণীকরণ করেছেন এবং প্রকল্পটি ইতিমধ্যে আপনার প্রকল্প আইডিতে সেট করা আছে।

  1. আপনি প্রমাণীকৃত কিনা তা নিশ্চিত করতে ক্লাউড শেলে নিম্নলিখিত কমান্ডটি চালান:
gcloud auth list

যদি আপনাকে একটি GCP API কল করার জন্য ক্লাউড শেলকে অনুমোদন করার জন্য অনুরোধ করা হয়, তাহলে অনুমোদন ক্লিক করুন।

কমান্ড আউটপুট

 Credentialed Accounts
ACTIVE  ACCOUNT
*       <my_account>@<my_domain.com>

সক্রিয় অ্যাকাউন্ট সেট করতে, চালান:

gcloud config set account <ACCOUNT>

ক্লাউড শেল-এ আপনি সঠিক প্রকল্প নির্বাচন করেছেন তা নিশ্চিত করতে, চালান:

gcloud config list project

কমান্ড আউটপুট

[core]
project = <PROJECT_ID>

যদি সঠিক প্রকল্পটি ফেরত না দেওয়া হয়, আপনি এই কমান্ডের সাথে এটি সেট করতে পারেন:

gcloud config set project <PROJECT_ID>

কমান্ড আউটপুট

Updated property [core/project].

কোডল্যাব কমান্ড লাইন অপারেশনের পাশাপাশি ফাইল সম্পাদনার মিশ্রণ ব্যবহার করে। ফাইল সম্পাদনার জন্য, আপনি ক্লাউড শেল টুলবারের ডানদিকে ওপেন এডিটর বোতামে ক্লিক করে ক্লাউড শেল-এ অন্তর্নির্মিত কোড সম্পাদক ব্যবহার করতে পারেন। আপনি ক্লাউড শেল-এ উপলব্ধ ভিম এবং ইমাক্সের মতো জনপ্রিয় সম্পাদকগুলিও পাবেন।

3. ক্লাউড রান, ডেটাস্টোর এবং অ্যাড-অন API সক্ষম করুন৷

ক্লাউড এপিআই সক্ষম করুন

ক্লাউড শেল থেকে, যে উপাদানগুলি ব্যবহার করা হবে তার জন্য ক্লাউড এপিআই সক্ষম করুন:

gcloud services enable \
  run.googleapis.com \
  cloudbuild.googleapis.com \
  cloudresourcemanager.googleapis.com \
  datastore.googleapis.com \
  gsuiteaddons.googleapis.com

এই অপারেশনটি সম্পূর্ণ হতে কয়েক মুহূর্ত সময় লাগতে পারে।

একবার সম্পন্ন হলে, এটির অনুরূপ একটি সাফল্যের বার্তা প্রদর্শিত হবে:

Operation "operations/acf.cc11852d-40af-47ad-9d59-477a12847c9e" finished successfully.

একটি ডেটাস্টোর উদাহরণ তৈরি করুন

এরপরে, অ্যাপ ইঞ্জিন সক্ষম করুন এবং একটি ডেটাস্টোর ডেটাবেস তৈরি করুন। ডেটাস্টোর ব্যবহার করার জন্য অ্যাপ ইঞ্জিন সক্ষম করা একটি পূর্বশর্ত, কিন্তু আমরা অন্য কিছুর জন্য অ্যাপ ইঞ্জিন ব্যবহার করব না।

gcloud app create --region=us-central
gcloud firestore databases create --type=datastore-mode --region=us-central

অ্যাড-অন চালানোর জন্য ব্যবহারকারীর অনুমতি প্রয়োজন এবং তাদের ডেটার উপর পদক্ষেপ নিতে হবে। এটি সক্ষম করতে প্রকল্পের সম্মতি স্ক্রীন কনফিগার করুন। কোডল্যাবের জন্য, আপনি সম্মতি স্ক্রিনটিকে একটি অভ্যন্তরীণ অ্যাপ্লিকেশন হিসাবে কনফিগার করবেন, যার অর্থ এটি সর্বজনীন বিতরণের জন্য নয়, শুরু করার জন্য।

  1. একটি নতুন ট্যাব বা উইন্ডোতে Google ক্লাউড কনসোল খুলুন।
  2. "Google ক্লাউড কনসোলের" পাশে, নিচের তীরটিতে ক্লিক করুন ড্রপ ডাউন তীর এবং আপনার প্রকল্প নির্বাচন করুন।
  3. উপরের বাম কোণে, মেনুতে ক্লিক করুন মেনু আইকন .
  4. APIs & Services > Credentials এ ক্লিক করুন। আপনার প্রকল্পের জন্য শংসাপত্র পৃষ্ঠা প্রদর্শিত হবে.
  5. OAuth সম্মতি স্ক্রীনে ক্লিক করুন। "OAuth সম্মতি স্ক্রীন" স্ক্রীনটি প্রদর্শিত হবে।
  6. "ব্যবহারকারীর প্রকার" এর অধীনে অভ্যন্তরীণ নির্বাচন করুন। যদি একটি @gmail.com অ্যাকাউন্ট ব্যবহার করেন, বহিরাগত নির্বাচন করুন।
  7. তৈরি করুন ক্লিক করুন। একটি "অ্যাপ নিবন্ধন সম্পাদনা করুন" পৃষ্ঠা প্রদর্শিত হবে৷
  8. ফরমটি পূরণ কর:
    • অ্যাপের নামে , "টুডো অ্যাড-অন" লিখুন।
    • ব্যবহারকারী সমর্থন ইমেলে , আপনার ব্যক্তিগত ইমেল ঠিকানা লিখুন।
    • বিকাশকারীর যোগাযোগের তথ্যের অধীনে, আপনার ব্যক্তিগত ইমেল ঠিকানা লিখুন।
  9. সংরক্ষণ করুন এবং চালিয়ে যান ক্লিক করুন। একটি স্কোপ ফর্ম প্রদর্শিত হয়.
  10. স্কোপ ফর্ম থেকে, সংরক্ষণ করুন এবং চালিয়ে যান ক্লিক করুন। একটি সারসংক্ষেপ প্রদর্শিত হয়.
  11. ড্যাশবোর্ডে ফিরে যান ক্লিক করুন।

4. প্রাথমিক অ্যাড-অন তৈরি করুন

প্রকল্পটি শুরু করুন

শুরু করতে, আপনি একটি সাধারণ "হ্যালো ওয়ার্ল্ড" অ্যাড-অন তৈরি করবেন এবং এটি স্থাপন করবেন৷ অ্যাড-অনগুলি হল ওয়েব পরিষেবা যা https অনুরোধে সাড়া দেয় এবং একটি JSON পেলোডের সাথে সাড়া দেয় যা UI এবং পদক্ষেপগুলি বর্ণনা করে। এই অ্যাড-অনে, আপনি Node.js এবং Express ফ্রেমওয়ার্ক ব্যবহার করবেন।

এই টেমপ্লেট প্রকল্পটি তৈরি করতে, ক্লাউড শেল ব্যবহার করে todo-add-on নামে একটি নতুন ডিরেক্টরি তৈরি করুন এবং এটিতে নেভিগেট করুন:

mkdir ~/todo-add-on
cd ~/todo-add-on

আপনি এই ডিরেক্টরিতে কোডল্যাবের জন্য সমস্ত কাজ করবেন।

Node.js প্রকল্প শুরু করুন:

npm init

NPM প্রকল্প কনফিগারেশন সম্পর্কে বিভিন্ন প্রশ্ন জিজ্ঞাসা করে, যেমন নাম এবং সংস্করণ। প্রতিটি প্রশ্নের জন্য, ডিফল্ট মান গ্রহণ করতে ENTER টিপুন। ডিফল্ট এন্ট্রি পয়েন্ট হল index.js নামের একটি ফাইল, যা আমরা পরবর্তীতে তৈরি করব।

পরবর্তী, এক্সপ্রেস ওয়েব ফ্রেমওয়ার্ক ইনস্টল করুন:

npm install --save express express-async-handler

অ্যাড-অন ব্যাকএন্ড তৈরি করুন

অ্যাপ তৈরি শুরু করার সময়।

index.js নামে একটি ফাইল তৈরি করুন। ফাইল তৈরি করতে, আপনি ক্লাউড শেল উইন্ডোর টুলবারে ওপেন এডিটর বোতামে ক্লিক করে ক্লাউড শেল এডিটর ব্যবহার করতে পারেন। বিকল্পভাবে, আপনি vim বা emacs ব্যবহার করে ক্লাউড শেলে ফাইলগুলি সম্পাদনা এবং পরিচালনা করতে পারেন।

আপনি index.js ফাইল তৈরি করার পরে, নিম্নলিখিত বিষয়বস্তু যোগ করুন:

const express = require('express');
const asyncHandler = require('express-async-handler');

// Create and configure the app
const app = express();

// Trust GCPs front end to for hostname/port forwarding
app.set("trust proxy", true);
app.use(express.json());

// Initial route for the add-on
app.post("/", asyncHandler(async (req, res) => {
    const card = {
        sections: [{
            widgets: [
                {
                    textParagraph: {
                        text: `Hello world!`
                    }
                },
            ]
        }]
    };
    const renderAction = {
        action: {
            navigations: [{
                pushCard: card
            }]
        }
    };
    res.json(renderAction);
}));

// Start the server
const port = process.env.PORT || 8080;
app.listen(port, () => {
  console.log(`Example app listening at http://localhost:${port}`)
});

সার্ভার 'হ্যালো ওয়ার্ল্ড' বার্তা দেখানো ছাড়া অন্য কিছু করে না এবং এটি ঠিক আছে। আপনি পরে আরও কার্যকারিতা যোগ করবেন।

ক্লাউড রানে স্থাপন করুন

ক্লাউড রানে স্থাপন করতে, অ্যাপটিকে কন্টেইনারাইজ করতে হবে।

ধারক তৈরি করুন

ডকারফাইল নামের একটি Dockerfile তৈরি করুন যাতে রয়েছে:

FROM node:12-slim

# Create and change to the app directory.
WORKDIR /usr/src/app

# Copy application dependency manifests to the container image.
# A wildcard is used to ensure copying both package.json AND package-lock.json (when available).
# Copying this first prevents re-running npm install on every code change.
COPY package*.json ./

# Install production dependencies.
# If you add a package-lock.json, speed your build by switching to 'npm ci'.
# RUN npm ci --only=production
RUN npm install --only=production

# Copy local code to the container image.
COPY . ./

# Run the web service on container startup.
CMD [ "node", "index.js" ]

অবাঞ্ছিত ফাইলগুলিকে পাত্রের বাইরে রাখুন

কন্টেইনার হালকা রাখতে সাহায্য করার জন্য, একটি .dockerignore ফাইল তৈরি করুন যাতে রয়েছে:

Dockerfile
.dockerignore
node_modules
npm-debug.log

ক্লাউড বিল্ড সক্ষম করুন

এই কোডল্যাবে আপনি নতুন কার্যকারিতা যুক্ত হওয়ার সাথে সাথে অ্যাড-অনটি কয়েকবার তৈরি এবং স্থাপন করবেন। কন্টেইনার তৈরি করার জন্য আলাদা কমান্ড চালানোর পরিবর্তে, এটিকে কন্টেইনার রেজিস্ট্রিতে ঠেলে দিন এবং ক্লাউড বিল্ডে স্থাপন করুন, পদ্ধতিটি অর্কেস্ট্রেট করতে ক্লাউড বিল্ড ব্যবহার করুন। অ্যাপ্লিকেশন তৈরি এবং স্থাপন করার নির্দেশাবলী সহ একটি cloudbuild.yaml ফাইল তৈরি করুন:

steps:
 # Build the container image
 - name: 'gcr.io/cloud-builders/docker'
   args: ['build', '-t', 'gcr.io/$PROJECT_ID/$_SERVICE_NAME', '.']
 # Push the container image to Container Registry
 - name: 'gcr.io/cloud-builders/docker'
   args: ['push', 'gcr.io/$PROJECT_ID/$_SERVICE_NAME']
 # Deploy container image to Cloud Run
 - name: 'gcr.io/google.com/cloudsdktool/cloud-sdk'
   entrypoint: gcloud
   args:
   - 'run'
   - 'deploy'
   - '$_SERVICE_NAME'
   - '--image'
   - 'gcr.io/$PROJECT_ID/$_SERVICE_NAME'
   - '--region'
   - '$_REGION'
   - '--platform'
   - 'managed'
images:
 - 'gcr.io/$PROJECT_ID/$_SERVICE_NAME'
substitutions:
   _SERVICE_NAME: todo-add-on
   _REGION: us-central1

অ্যাপটি স্থাপন করার জন্য ক্লাউড বিল্ডকে অনুমতি দিতে নিম্নলিখিত কমান্ডগুলি চালান:

PROJECT_ID=$(gcloud config list --format='value(core.project)')
PROJECT_NUMBER=$(gcloud projects describe $PROJECT_ID --format='value(projectNumber)')
gcloud projects add-iam-policy-binding $PROJECT_ID \
    --member=serviceAccount:$PROJECT_NUMBER@cloudbuild.gserviceaccount.com \
    --role=roles/run.admin
gcloud iam service-accounts add-iam-policy-binding \
    $PROJECT_NUMBER-compute@developer.gserviceaccount.com \
    --member=serviceAccount:$PROJECT_NUMBER@cloudbuild.gserviceaccount.com \
    --role=roles/iam.serviceAccountUser

অ্যাড-অন ব্যাকএন্ড তৈরি করুন এবং স্থাপন করুন

বিল্ড শুরু করতে, ক্লাউড শেলে, চালান:

gcloud builds submit

সম্পূর্ণ বিল্ড এবং ডিপ্লোয় সম্পূর্ণ হতে কয়েক মিনিট সময় লাগতে পারে, বিশেষ করে প্রথমবার।

একবার বিল্ড সম্পূর্ণ হলে, পরিষেবাটি স্থাপন করা হয়েছে তা যাচাই করুন এবং URLটি খুঁজুন। কমান্ড চালান:

gcloud run services list --platform managed

এই ইউআরএলটি কপি করুন, পরবর্তী ধাপের জন্য আপনার এটি প্রয়োজন হবে – কীভাবে অ্যাড-অন চালু করতে হয় তা Google Workspaceকে বলুন।

অ্যাড-অন নিবন্ধন করুন

এখন যেহেতু সার্ভার চালু হয়ে গেছে, অ্যাড-অনের বর্ণনা দিন যাতে Google Workspace এটিকে কীভাবে প্রদর্শন করতে এবং চালু করতে হয় তা জানে।

একটি স্থাপনার বর্ণনা তৈরি করুন

নিম্নলিখিত বিষয়বস্তু দিয়ে deployment.json ফাইলটি তৈরি করুন। URL প্লেসহোল্ডারের জায়গায় স্থাপন করা অ্যাপের ইউআরএল ব্যবহার করা নিশ্চিত করুন।

{
  "oauthScopes": [
    "https://www.googleapis.com/auth/gmail.addons.execute",
    "https://www.googleapis.com/auth/calendar.addons.execute"
  ],
  "addOns": {
    "common": {
      "name": "Todo Codelab",
      "logoUrl": "https://raw.githubusercontent.com/webdog/octicons-png/main/black/check.png",
      "homepageTrigger": {
        "runFunction": "URL"
      }
    },
    "gmail": {},
    "drive": {},
    "calendar": {},
    "docs": {},
    "sheets": {},
    "slides": {}
  }
}

কমান্ড চালানোর মাধ্যমে স্থাপনার বর্ণনা আপলোড করুন:

gcloud workspace-add-ons deployments create todo-add-on --deployment-file=deployment.json

অ্যাড-অন ব্যাকএন্ডে অ্যাক্সেস অনুমোদন করুন

অ্যাড-অন ফ্রেমওয়ার্কেরও পরিষেবাতে কল করার অনুমতি প্রয়োজন। Google Workspace-কে অ্যাড-অন চালু করার অনুমতি দিতে Cloud Ruনের জন্য IAM নীতি আপডেট করতে নিম্নলিখিত কমান্ডগুলি চালান:

SERVICE_ACCOUNT_EMAIL=$(gcloud workspace-add-ons get-authorization --format="value(serviceAccountEmail)")
gcloud run services add-iam-policy-binding todo-add-on --platform managed --region us-central1 --role roles/run.invoker --member "serviceAccount:$SERVICE_ACCOUNT_EMAIL"

পরীক্ষার জন্য অ্যাড-অন ইনস্টল করুন

আপনার অ্যাকাউন্টের জন্য ডেভেলপমেন্ট মোডে অ্যাড-অন ইনস্টল করতে, ক্লাউড শেলে, চালান:

gcloud workspace-add-ons deployments install todo-add-on

একটি নতুন ট্যাব বা উইন্ডোতে (Gmail)[https://mail.google.com/] খুলুন। ডানদিকে, একটি চেকমার্ক আইকন সহ অ্যাড-অন খুঁজুন।

ইনস্টল করা অ্যাড-অন আইকন

অ্যাড-অন খুলতে, চেকমার্ক আইকনে ক্লিক করুন। অ্যাড-অন অনুমোদন করার জন্য একটি প্রম্পট প্রদর্শিত হবে।

অনুমোদন প্রম্পট

অ্যাক্সেস অনুমোদন ক্লিক করুন এবং পপআপে অনুমোদন প্রবাহ নির্দেশাবলী অনুসরণ করুন। একবার সম্পূর্ণ হয়ে গেলে, অ্যাড-অন স্বয়ংক্রিয়ভাবে পুনরায় লোড হয় এবং 'হ্যালো ওয়ার্ল্ড!' বার্তা

অভিনন্দন! আপনার কাছে এখন একটি সাধারণ অ্যাড-অন স্থাপন এবং ইনস্টল করা আছে। এটি একটি টাস্ক লিস্ট অ্যাপ্লিকেশনে পরিণত করার সময়!

5. ব্যবহারকারীর পরিচয় অ্যাক্সেস করুন

অ্যাড-অনগুলি সাধারণত অনেক ব্যবহারকারী তাদের বা তাদের সংস্থার ব্যক্তিগত তথ্য নিয়ে কাজ করতে ব্যবহার করে। এই কোডল্যাবে, অ্যাড-অনটি শুধুমাত্র বর্তমান ব্যবহারকারীর জন্য কাজগুলি প্রদর্শন করবে। ব্যবহারকারীর পরিচয় একটি পরিচয় টোকেনের মাধ্যমে অ্যাড-অনে পাঠানো হয় যা ডিকোড করা প্রয়োজন।

স্থাপনার বর্ণনাকারীতে স্কোপ যোগ করুন

ব্যবহারকারীর পরিচয় ডিফল্টরূপে পাঠানো হয় না। এটি ব্যবহারকারীর ডেটা এবং এটি অ্যাক্সেস করার জন্য অ্যাড-অনের অনুমতি প্রয়োজন৷ সেই অনুমতি পেতে, deployment.json আপডেট করুন এবং অ্যাড-অনের প্রয়োজনীয় স্কোপের তালিকায় openid এবং email OAuth স্কোপ যোগ করুন। OAuth স্কোপ যোগ করার পর, অ্যাড-অন ব্যবহারকারীদের পরের বার অ্যাড-অন ব্যবহার করার সময় অ্যাক্সেস দেওয়ার জন্য অনুরোধ করে।

"oauthScopes": [
      "https://www.googleapis.com/auth/gmail.addons.execute",
      "https://www.googleapis.com/auth/calendar.addons.execute",
      "openid",
      "email"
],

তারপর, ক্লাউড শেল-এ, স্থাপনার বিবরণ আপডেট করতে এই কমান্ডটি চালান:

gcloud workspace-add-ons deployments replace todo-add-on --deployment-file=deployment.json

অ্যাড-অন সার্ভার আপডেট করুন

অ্যাড-অনটি ব্যবহারকারীর পরিচয়ের অনুরোধ করার জন্য কনফিগার করা হলেও, বাস্তবায়নটি এখনও আপডেট করা দরকার।

পরিচয় টোকেন পার্স করুন

প্রোজেক্টে Google auth লাইব্রেরি যোগ করে শুরু করুন:

npm install --save google-auth-library

তারপর OAuth2Client প্রয়োজনের জন্য index.js সম্পাদনা করুন:

const { OAuth2Client } = require('google-auth-library');

তারপর আইডি টোকেন পার্স করতে একটি সহায়ক পদ্ধতি যোগ করুন:

async function userInfo(event) {
    const idToken = event.authorizationEventObject.userIdToken;
    const authClient = new OAuth2Client();
    const ticket = await authClient.verifyIdToken({
        idToken
    });
    return ticket.getPayload();
}

ব্যবহারকারীর পরিচয় প্রদর্শন করুন

টাস্ক তালিকার সমস্ত কার্যকারিতা যোগ করার আগে এটি একটি চেকপয়েন্টের জন্য একটি ভাল সময়। 'হ্যালো ওয়ার্ল্ড'-এর পরিবর্তে ব্যবহারকারীর ইমেল ঠিকানা এবং অনন্য আইডি প্রিন্ট করতে অ্যাপের রুট আপডেট করুন।

app.post('/', asyncHandler(async (req, res) => {
    const event = req.body;
    const user = await userInfo(event);
    const card = {
        sections: [{
            widgets: [
                {
                    textParagraph: {
                        text: `Hello ${user.email} ${user.sub}`
                    }
                },
            ]
        }]
    };
    const renderAction = {
        action: {
            navigations: [{
                pushCard: card
            }]
        }
    };
    res.json(renderAction);
}));

এই পরিবর্তনের পরে, ফলস্বরূপ index.js ফাইলটি দেখতে হবে:

const express = require('express');
const asyncHandler = require('express-async-handler');
const { OAuth2Client } = require('google-auth-library');

// Create and configure the app
const app = express();

// Trust GCPs front end to for hostname/port forwarding
app.set("trust proxy", true);
app.use(express.json());

// Initial route for the add-on
app.post('/', asyncHandler(async (req, res) => {
    const event = req.body;
    const user = await userInfo(event);
    const card = {
        sections: [{
            widgets: [
                {
                    textParagraph: {
                        text: `Hello ${user.email} ${user.sub}`
                    }
                },
            ]
        }]
    };
    const renderAction = {
        action: {
            navigations: [{
                pushCard: card
            }]
        }
    };
    res.json(renderAction);
}));

async function userInfo(event) {
    const idToken = event.authorizationEventObject.userIdToken;
    const authClient = new OAuth2Client();
    const ticket = await authClient.verifyIdToken({
        idToken
    });
    return ticket.getPayload();
}

// Start the server
const port = process.env.PORT || 8080;
app.listen(port, () => {
  console.log(`Example app listening at http://localhost:${port}`)
});

পুনরায় স্থাপন এবং পরীক্ষা

অ্যাডঅন পুনর্নির্মাণ এবং পুনরায় স্থাপন করুন। ক্লাউড শেল থেকে, চালান:

gcloud builds submit

সার্ভার পুনরায় স্থাপন করা হলে, Gmail খুলুন বা পুনরায় লোড করুন এবং আবার অ্যাড-অন খুলুন। যেহেতু সুযোগগুলি পরিবর্তিত হয়েছে, অ্যাড-অনটি পুনরায় অনুমোদনের জন্য জিজ্ঞাসা করবে৷ অ্যাড-অনটিকে আবার অনুমোদন করুন এবং একবার অ্যাড-অনটি সম্পূর্ণ করলে আপনার ইমেল ঠিকানা এবং ব্যবহারকারীর আইডি দেখাবে।

এখন অ্যাড-অন জানে যে ব্যবহারকারী কে, আপনি টাস্ক তালিকা কার্যকারিতা যোগ করা শুরু করতে পারেন।

6. টাস্ক লিস্ট বাস্তবায়ন করুন

কোডল্যাবের প্রাথমিক ডেটা মডেলটি সহজবোধ্য: Task এন্টিটির একটি তালিকা, প্রতিটিতে টাস্ক বর্ণনামূলক পাঠ্য এবং একটি টাইমস্ট্যাম্পের বৈশিষ্ট্য রয়েছে।

ডেটাস্টোর সূচক তৈরি করুন

কোডল্যাবে আগে থেকেই প্রকল্পের জন্য ডেটাস্টোর সক্ষম করা হয়েছিল। এটির একটি স্কিমার প্রয়োজন নেই, যদিও এটি যৌগিক প্রশ্নের জন্য স্পষ্টভাবে সূচী তৈরি করতে হবে। সূচী তৈরি করতে কয়েক মিনিট সময় লাগতে পারে, তাই আপনি প্রথমে এটি করবেন।

নিম্নলিখিত দিয়ে index.yaml নামে একটি ফাইল তৈরি করুন:

indexes:
- kind: Task
  ancestor: yes
  properties:
  - name: created

তারপর Datastore সূচী আপডেট করুন:

gcloud datastore indexes create index.yaml

চালিয়ে যাওয়ার জন্য অনুরোধ করা হলে, আপনার কীবোর্ডে ENTER টিপুন। সূচক তৈরি পটভূমিতে ঘটে। যখন এটি ঘটছে, তখন "todos" বাস্তবায়ন করতে অ্যাড-অন কোড আপডেট করা শুরু করুন।

অ্যাড-অন ব্যাকএন্ড আপডেট করুন

প্রকল্পে ডেটাস্টোর লাইব্রেরি ইনস্টল করুন:

npm install --save @google-cloud/datastore

ডেটাস্টোরে পড়ুন এবং লিখুন

ডেটাস্টোর লাইব্রেরি আমদানি এবং ক্লায়েন্ট তৈরি করার সাথে শুরু করে "todos" বাস্তবায়ন করতে index.js আপডেট করুন:

const {Datastore} = require('@google-cloud/datastore');
const datastore = new Datastore();

ডেটাস্টোর থেকে কাজগুলি পড়তে এবং লেখার পদ্ধতি যুক্ত করুন:

async function listTasks(userId) {
    const parentKey = datastore.key(['User', userId]);
    const query = datastore.createQuery('Task')
        .hasAncestor(parentKey)
        .order('created')
        .limit(20);
    const [tasks] = await datastore.runQuery(query);
    return tasks;;
}

async function addTask(userId, task) {
    const key = datastore.key(['User', userId, 'Task']);
    const entity = {
        key,
        data: task,
    };
    await datastore.save(entity);
    return entity;
}

async function deleteTasks(userId, taskIds) {
    const keys = taskIds.map(id => datastore.key(['User', userId,
                                                  'Task', datastore.int(id)]));
    await datastore.delete(keys);
}

UI এর রেন্ডারিং প্রয়োগ করুন

বেশিরভাগ পরিবর্তন অ্যাড-অন UI-তে। পূর্বে, UI দ্বারা প্রত্যাবর্তিত সমস্ত কার্ড স্থির ছিল – উপলব্ধ ডেটার উপর নির্ভর করে সেগুলি পরিবর্তিত হয়নি। এখানে, ব্যবহারকারীর বর্তমান টাস্ক লিস্টের উপর ভিত্তি করে কার্ডটিকে গতিশীলভাবে তৈরি করতে হবে।

কোডল্যাবের জন্য UI-তে একটি টেক্সট ইনপুট থাকে এবং সেগুলিকে সম্পূর্ণ চিহ্নিত করার জন্য চেক বক্স সহ কাজের তালিকা থাকে। এগুলির প্রত্যেকটিতে একটি onChangeAction প্রপার্টি রয়েছে যার ফলে ব্যবহারকারী একটি টাস্ক যুক্ত বা মুছে দিলে অ্যাড-অন সার্ভারে কলব্যাক করে। এই প্রতিটি ক্ষেত্রে, UI কে আপডেট করা টাস্ক তালিকার সাথে পুনরায় রেন্ডার করা দরকার। এটি পরিচালনা করতে, কার্ড UI তৈরির জন্য একটি নতুন পদ্ধতি চালু করা যাক।

index.js সম্পাদনা চালিয়ে যান এবং নিম্নলিখিত পদ্ধতি যোগ করুন:

function buildCard(req, tasks) {
    const baseUrl = `${req.protocol}://${req.hostname}${req.baseUrl}`;
    // Input for adding a new task
    const inputSection = {
        widgets: [
            {
                textInput: {
                    label: 'Task to add',
                    name: 'newTask',
                    value: '',
                    onChangeAction: {
                        function: `${baseUrl}/newTask`,
                    },
                }
            }
        ]
    };
    const taskListSection = {
        header: 'Your tasks',
        widgets: []
    };
    if (tasks && tasks.length) {
        // Create text & checkbox for each task
        tasks.forEach(task => taskListSection.widgets.push({
            decoratedText: {
                text: task.text,
                wrapText: true,
                switchControl: {
                    controlType: 'CHECKBOX',
                    name: 'completedTasks',
                    value: task[datastore.KEY].id,
                    selected: false,
                    onChangeAction: {
                        function: `${baseUrl}/complete`,
                    }
                }
            }
        }));
    } else {
        // Placeholder for empty task list
        taskListSection.widgets.push({
            textParagraph: {
                text: 'Your task list is empty.'
            }
        });
    }
    const card = {
        sections: [
            inputSection,
            taskListSection,
        ]
    }
    return card;
}

রুট আপডেট করুন

এখন যেহেতু ডেটাস্টোরে পড়তে এবং লিখতে এবং UI তৈরি করার সহায়ক পদ্ধতি রয়েছে, আসুন অ্যাপ রুটে সেগুলিকে একত্রিত করি। বিদ্যমান রুটটি প্রতিস্থাপন করুন এবং আরও দুটি যোগ করুন: একটি কাজ যোগ করার জন্য এবং একটি মুছে ফেলার জন্য৷

app.post('/', asyncHandler(async (req, res) => {
    const event = req.body;
    const user = await userInfo(event);
    const tasks = await listTasks(user.sub);
    const card = buildCard(req, tasks);
    const responsePayload = {
        action: {
            navigations: [{
                pushCard: card
            }]
        }
    };
    res.json(responsePayload);
}));

app.post('/newTask', asyncHandler(async (req, res) => {
    const event = req.body;
    const user = await userInfo(event);

    const formInputs = event.commonEventObject.formInputs || {};
    const newTask = formInputs.newTask;
    if (!newTask || !newTask.stringInputs) {
        return {};
    }

    const task = {
        text: newTask.stringInputs.value[0],
        created: new Date()
    };
    await addTask(user.sub, task);

    const tasks = await listTasks(user.sub);
    const card = buildCard(req, tasks);
    const responsePayload = {
        renderActions: {
            action: {
                navigations: [{
                    updateCard: card
                }],
                notification: {
                    text: 'Task added.'
                },
            }
        }
    };
    res.json(responsePayload);
}));

app.post('/complete', asyncHandler(async (req, res) => {
    const event = req.body;
    const user = await userInfo(event);

    const formInputs = event.commonEventObject.formInputs || {};
    const completedTasks = formInputs.completedTasks;
    if (!completedTasks || !completedTasks.stringInputs) {
        return {};
    }

    await deleteTasks(user.sub, completedTasks.stringInputs.value);

    const tasks = await listTasks(user.sub);
    const card = buildCard(req, tasks);
    const responsePayload = {
        renderActions: {
            action: {
                navigations: [{
                    updateCard: card
                }],
                notification: {
                    text: 'Task completed.'
                },
            }
        }
    };
    res.json(responsePayload);
}));

এখানে চূড়ান্ত, সম্পূর্ণ কার্যকরী index.js ফাইল আছে:

const express = require('express');
const asyncHandler = require('express-async-handler');
const { OAuth2Client } = require('google-auth-library');
const {Datastore} = require('@google-cloud/datastore');
const datastore = new Datastore();

// Create and configure the app
const app = express();

// Trust GCPs front end to for hostname/port forwarding
app.set("trust proxy", true);
app.use(express.json());

// Initial route for the add-on
app.post('/', asyncHandler(async (req, res) => {
    const event = req.body;
    const user = await userInfo(event);
    const tasks = await listTasks(user.sub);
    const card = buildCard(req, tasks);
    const responsePayload = {
        action: {
            navigations: [{
                pushCard: card
            }]
        }
    };
    res.json(responsePayload);
}));

app.post('/newTask', asyncHandler(async (req, res) => {
    const event = req.body;
    const user = await userInfo(event);

    const formInputs = event.commonEventObject.formInputs || {};
    const newTask = formInputs.newTask;
    if (!newTask || !newTask.stringInputs) {
        return {};
    }

    const task = {
        text: newTask.stringInputs.value[0],
        created: new Date()
    };
    await addTask(user.sub, task);

    const tasks = await listTasks(user.sub);
    const card = buildCard(req, tasks);
    const responsePayload = {
        renderActions: {
            action: {
                navigations: [{
                    updateCard: card
                }],
                notification: {
                    text: 'Task added.'
                },
            }
        }
    };
    res.json(responsePayload);
}));

app.post('/complete', asyncHandler(async (req, res) => {
    const event = req.body;
    const user = await userInfo(event);

    const formInputs = event.commonEventObject.formInputs || {};
    const completedTasks = formInputs.completedTasks;
    if (!completedTasks || !completedTasks.stringInputs) {
        return {};
    }

    await deleteTasks(user.sub, completedTasks.stringInputs.value);

    const tasks = await listTasks(user.sub);
    const card = buildCard(req, tasks);
    const responsePayload = {
        renderActions: {
            action: {
                navigations: [{
                    updateCard: card
                }],
                notification: {
                    text: 'Task completed.'
                },
            }
        }
    };
    res.json(responsePayload);
}));

function buildCard(req, tasks) {
    const baseUrl = `${req.protocol}://${req.hostname}${req.baseUrl}`;
    // Input for adding a new task
    const inputSection = {
        widgets: [
            {
                textInput: {
                    label: 'Task to add',
                    name: 'newTask',
                    value: '',
                    onChangeAction: {
                        function: `${baseUrl}/newTask`,
                    },
                }
            }
        ]
    };
    const taskListSection = {
        header: 'Your tasks',
        widgets: []
    };
    if (tasks && tasks.length) {
        // Create text & checkbox for each task
        tasks.forEach(task => taskListSection.widgets.push({
            decoratedText: {
                text: task.text,
                wrapText: true,
                switchControl: {
                    controlType: 'CHECKBOX',
                    name: 'completedTasks',
                    value: task[datastore.KEY].id,
                    selected: false,
                    onChangeAction: {
                        function: `${baseUrl}/complete`,
                    }
                }
            }
        }));
    } else {
        // Placeholder for empty task list
        taskListSection.widgets.push({
            textParagraph: {
                text: 'Your task list is empty.'
            }
        });
    }
    const card = {
        sections: [
            inputSection,
            taskListSection,
        ]
    }
    return card;
}

async function userInfo(event) {
    const idToken = event.authorizationEventObject.userIdToken;
    const authClient = new OAuth2Client();
    const ticket = await authClient.verifyIdToken({
        idToken
    });
    return ticket.getPayload();
}

async function listTasks(userId) {
    const parentKey = datastore.key(['User', userId]);
    const query = datastore.createQuery('Task')
        .hasAncestor(parentKey)
        .order('created')
        .limit(20);
    const [tasks] = await datastore.runQuery(query);
    return tasks;;
}

async function addTask(userId, task) {
    const key = datastore.key(['User', userId, 'Task']);
    const entity = {
        key,
        data: task,
    };
    await datastore.save(entity);
    return entity;
}

async function deleteTasks(userId, taskIds) {
    const keys = taskIds.map(id => datastore.key(['User', userId,
                                                  'Task', datastore.int(id)]));
    await datastore.delete(keys);
}

// Start the server
const port = process.env.PORT || 8080;
app.listen(port, () => {
  console.log(`Example app listening at http://localhost:${port}`)
});

পুনরায় স্থাপন এবং পরীক্ষা

অ্যাড-অন পুনর্নির্মাণ এবং পুনরায় স্থাপন করতে, একটি বিল্ড শুরু করুন। ক্লাউড শেলে, চালান:

gcloud builds submit

Gmail এ, অ্যাড-অনটি পুনরায় লোড করুন এবং নতুন UI প্রদর্শিত হবে। অ্যাড-অনটি অন্বেষণ করতে এক মিনিট সময় নিন। ইনপুটে কিছু পাঠ্য প্রবেশ করান এবং আপনার কীবোর্ডে ENTER টিপে কয়েকটি কাজ যুক্ত করুন, তারপরে সেগুলি মুছতে চেকবক্সে ক্লিক করুন৷

কাজ সহ অ্যাড-অন

আপনি যদি চান, আপনি এই কোডল্যাবের চূড়ান্ত ধাপে এগিয়ে যেতে পারেন এবং আপনার প্রকল্প পরিষ্কার করতে পারেন। অথবা, আপনি যদি অ্যাড-অন সম্পর্কে আরও শেখা চালিয়ে যেতে চান, তাহলে আরও একটি ধাপ আপনি সম্পূর্ণ করতে পারেন।

7. (ঐচ্ছিক) প্রসঙ্গ যোগ করা

অ্যাড-অনগুলির সবচেয়ে শক্তিশালী বৈশিষ্ট্যগুলির মধ্যে একটি হল প্রসঙ্গ-সচেতনতা। অ্যাড-অনগুলি ব্যবহারকারীর অনুমতি নিয়ে Google Workspace প্রসঙ্গে অ্যাক্সেস করতে পারে যেমন একজন ব্যবহারকারী যে ইমেলটি দেখছেন, একটি ক্যালেন্ডার ইভেন্ট এবং একটি নথি। অ্যাড-অনগুলিও বিষয়বস্তু সন্নিবেশ করার মতো পদক্ষেপ নিতে পারে। এই কোডল্যাবে, আপনি ওয়ার্কস্পেস এডিটরগুলির জন্য প্রসঙ্গ সমর্থন যোগ করবেন (ডক্স, শীট এবং স্লাইড) এডিটর থাকাকালীন তৈরি করা যেকোনো কাজের সাথে বর্তমান নথি সংযুক্ত করতে। যখন টাস্কটি প্রদর্শিত হয়, তখন এটিতে ক্লিক করলে একটি নতুন ট্যাবে ডকুমেন্টটি খুলবে যাতে ব্যবহারকারীকে তাদের কাজ শেষ করতে ডকুমেন্টে ফিরিয়ে আনতে পারে।

অ্যাড-অন ব্যাকএন্ড আপডেট করুন

newTask রুট আপডেট করুন

প্রথমে, একটি টাস্কে ডকুমেন্ট আইডি অন্তর্ভুক্ত করতে /newTask রুটটি আপডেট করুন যদি এটি উপলব্ধ থাকে:

app.post('/newTask', asyncHandler(async (req, res) => {
    const event = req.body;
    const user = await userInfo(event);

    const formInputs = event.commonEventObject.formInputs || {};
    const newTask = formInputs.newTask;
    if (!newTask || !newTask.stringInputs) {
        return {};
    }

    // Get the current document if it is present
    const editorInfo = event.docs || event.sheets || event.slides;
    let document = null;
    if (editorInfo && editorInfo.id) {
        document = {
            id: editorInfo.id,
        }
    }
    const task = {
        text: newTask.stringInputs.value[0],
        created: new Date(),
        document,
    };
    await addTask(user.sub, task);

    const tasks = await listTasks(user.sub);
    const card = buildCard(req, tasks);
    const responsePayload = {
        renderActions: {
            action: {
                navigations: [{
                    updateCard: card
                }],
                notification: {
                    text: 'Task added.'
                },
            }
        }
    };
    res.json(responsePayload);
}));

নতুন তৈরি কাজ এখন বর্তমান নথি আইডি অন্তর্ভুক্ত. যাইহোক, সম্পাদকদের মধ্যে প্রসঙ্গ ডিফল্টরূপে ভাগ করা হয় না। অন্যান্য ব্যবহারকারীর ডেটার মতো, ব্যবহারকারীকে ডেটা অ্যাক্সেস করার জন্য অ্যাড-অনের জন্য অনুমতি দিতে হবে। তথ্যের অত্যধিক ভাগাভাগি রোধ করতে, পছন্দের পদ্ধতি হল প্রতি-ফাইলের ভিত্তিতে অনুরোধ করা এবং অনুমতি দেওয়া।

UI আপডেট করুন

index.js এ, দুটি পরিবর্তন করতে buildCard আপডেট করুন। প্রথমটি যদি উপস্থিত থাকে তবে নথিতে একটি লিঙ্ক অন্তর্ভুক্ত করার জন্য কার্যগুলির রেন্ডারিং আপডেট করা হচ্ছে। দ্বিতীয়টি হল একটি ঐচ্ছিক অনুমোদন প্রম্পট প্রদর্শন করা যদি অ্যাড-অনটি একটি সম্পাদকে রেন্ডার করা হয় এবং ফাইল অ্যাক্সেস এখনও মঞ্জুর করা না হয়।

function buildCard(req, tasks) {
    const baseUrl = `${req.protocol}://${req.hostname}${req.baseUrl}`;
    const inputSection = {
        widgets: [
            {
                textInput: {
                    label: 'Task to add',
                    name: 'newTask',
                    value: '',
                    onChangeAction: {
                        function: `${baseUrl}/newTask`,
                    },
                }
            }
        ]
    };
    const taskListSection = {
        header: 'Your tasks',
        widgets: []
    };
    if (tasks && tasks.length) {
        tasks.forEach(task => {
            const widget = {
                decoratedText: {
                    text: task.text,
                    wrapText: true,
                    switchControl: {
                        controlType: 'CHECKBOX',
                        name: 'completedTasks',
                        value: task[datastore.KEY].id,
                        selected: false,
                        onChangeAction: {
                            function: `${baseUrl}/complete`,
                        }
                    }
                }
            };
            // Make item clickable and open attached doc if present
            if (task.document) {
                widget.decoratedText.bottomLabel = 'Click to open  document.';
                const id = task.document.id;
                const url = `https://drive.google.com/open?id=${id}`
                widget.decoratedText.onClick = {
                    openLink: {
                        openAs: 'FULL_SIZE',
                        onClose: 'NOTHING',
                        url: url,
                    }
                }
            }
            taskListSection.widgets.push(widget)
        });
    } else {
        taskListSection.widgets.push({
            textParagraph: {
                text: 'Your task list is empty.'
            }
        });
    }
    const card = {
        sections: [
            inputSection,
            taskListSection,
        ]
    };

    // Display file authorization prompt if the host is an editor
    // and no doc ID present
    const event = req.body;
    const editorInfo = event.docs || event.sheets || event.slides;
    const showFileAuth = editorInfo && editorInfo.id === undefined;
    if (showFileAuth) {
        card.fixedFooter = {
            primaryButton: {
                text: 'Authorize file access',
                onClick: {
                    action: {
                        function: `${baseUrl}/authorizeFile`,
                    }
                }
            }
        }
    }
    return card;
}

ফাইল অনুমোদন রুট বাস্তবায়ন

অনুমোদন বোতামটি অ্যাপে একটি নতুন রুট যোগ করে, তাই আসুন এটি বাস্তবায়ন করি। এই রুটটি একটি নতুন ধারণা, হোস্ট অ্যাপ অ্যাকশন প্রবর্তন করে। অ্যাড-অনের হোস্ট অ্যাপ্লিকেশনের সাথে ইন্টারঅ্যাক্ট করার জন্য এগুলি বিশেষ নির্দেশাবলী। এই ক্ষেত্রে, বর্তমান সম্পাদক ফাইলে অ্যাক্সেসের অনুরোধ করতে।

index.js এ, /authorizeFile রুট যোগ করুন:

app.post('/authorizeFile', asyncHandler(async (req, res) => {
    const responsePayload = {
        renderActions: {
            hostAppAction: {
                editorAction: {
                    requestFileScopeForActiveDocument: {}
                }
            },
        }
    };
    res.json(responsePayload);
}));

এখানে চূড়ান্ত, সম্পূর্ণ কার্যকরী index.js ফাইল আছে:

const express = require('express');
const asyncHandler = require('express-async-handler');
const { OAuth2Client } = require('google-auth-library');
const {Datastore} = require('@google-cloud/datastore');
const datastore = new Datastore();

// Create and configure the app
const app = express();

// Trust GCPs front end to for hostname/port forwarding
app.set("trust proxy", true);
app.use(express.json());

// Initial route for the add-on
app.post('/', asyncHandler(async (req, res) => {
    const event = req.body;
    const user = await userInfo(event);
    const tasks = await listTasks(user.sub);
    const card = buildCard(req, tasks);
    const responsePayload = {
        action: {
            navigations: [{
                pushCard: card
            }]
        }
    };
    res.json(responsePayload);
}));

app.post('/newTask', asyncHandler(async (req, res) => {
    const event = req.body;
    const user = await userInfo(event);

    const formInputs = event.commonEventObject.formInputs || {};
    const newTask = formInputs.newTask;
    if (!newTask || !newTask.stringInputs) {
        return {};
    }

    // Get the current document if it is present
    const editorInfo = event.docs || event.sheets || event.slides;
    let document = null;
    if (editorInfo && editorInfo.id) {
        document = {
            id: editorInfo.id,
        }
    }
    const task = {
        text: newTask.stringInputs.value[0],
        created: new Date(),
        document,
    };
    await addTask(user.sub, task);

    const tasks = await listTasks(user.sub);
    const card = buildCard(req, tasks);
    const responsePayload = {
        renderActions: {
            action: {
                navigations: [{
                    updateCard: card
                }],
                notification: {
                    text: 'Task added.'
                },
            }
        }
    };
    res.json(responsePayload);
}));

app.post('/complete', asyncHandler(async (req, res) => {
    const event = req.body;
    const user = await userInfo(event);

    const formInputs = event.commonEventObject.formInputs || {};
    const completedTasks = formInputs.completedTasks;
    if (!completedTasks || !completedTasks.stringInputs) {
        return {};
    }

    await deleteTasks(user.sub, completedTasks.stringInputs.value);

    const tasks = await listTasks(user.sub);
    const card = buildCard(req, tasks);
    const responsePayload = {
        renderActions: {
            action: {
                navigations: [{
                    updateCard: card
                }],
                notification: {
                    text: 'Task completed.'
                },
            }
        }
    };
    res.json(responsePayload);
}));

app.post('/authorizeFile', asyncHandler(async (req, res) => {
    const responsePayload = {
        renderActions: {
            hostAppAction: {
                editorAction: {
                    requestFileScopeForActiveDocument: {}
                }
            },
        }
    };
    res.json(responsePayload);
}));

function buildCard(req, tasks) {
    const baseUrl = `${req.protocol}://${req.hostname}${req.baseUrl}`;
    const inputSection = {
        widgets: [
            {
                textInput: {
                    label: 'Task to add',
                    name: 'newTask',
                    value: '',
                    onChangeAction: {
                        function: `${baseUrl}/newTask`,
                    },
                }
            }
        ]
    };
    const taskListSection = {
        header: 'Your tasks',
        widgets: []
    };
    if (tasks && tasks.length) {
        tasks.forEach(task => {
            const widget = {
                decoratedText: {
                    text: task.text,
                    wrapText: true,
                    switchControl: {
                        controlType: 'CHECKBOX',
                        name: 'completedTasks',
                        value: task[datastore.KEY].id,
                        selected: false,
                        onChangeAction: {
                            function: `${baseUrl}/complete`,
                        }
                    }
                }
            };
            // Make item clickable and open attached doc if present
            if (task.document) {
                widget.decoratedText.bottomLabel = 'Click to open  document.';
                const id = task.document.id;
                const url = `https://drive.google.com/open?id=${id}`
                widget.decoratedText.onClick = {
                    openLink: {
                        openAs: 'FULL_SIZE',
                        onClose: 'NOTHING',
                        url: url,
                    }
                }
            }
            taskListSection.widgets.push(widget)
        });
    } else {
        taskListSection.widgets.push({
            textParagraph: {
                text: 'Your task list is empty.'
            }
        });
    }
    const card = {
        sections: [
            inputSection,
            taskListSection,
        ]
    };

    // Display file authorization prompt if the host is an editor
    // and no doc ID present
    const event = req.body;
    const editorInfo = event.docs || event.sheets || event.slides;
    const showFileAuth = editorInfo && editorInfo.id === undefined;
    if (showFileAuth) {
        card.fixedFooter = {
            primaryButton: {
                text: 'Authorize file access',
                onClick: {
                    action: {
                        function: `${baseUrl}/authorizeFile`,
                    }
                }
            }
        }
    }
    return card;
}

async function userInfo(event) {
    const idToken = event.authorizationEventObject.userIdToken;
    const authClient = new OAuth2Client();
    const ticket = await authClient.verifyIdToken({
        idToken
    });
    return ticket.getPayload();
}

async function listTasks(userId) {
    const parentKey = datastore.key(['User', userId]);
    const query = datastore.createQuery('Task')
        .hasAncestor(parentKey)
        .order('created')
        .limit(20);
    const [tasks] = await datastore.runQuery(query);
    return tasks;;
}

async function addTask(userId, task) {
    const key = datastore.key(['User', userId, 'Task']);
    const entity = {
        key,
        data: task,
    };
    await datastore.save(entity);
    return entity;
}

async function deleteTasks(userId, taskIds) {
    const keys = taskIds.map(id => datastore.key(['User', userId,
                                                  'Task', datastore.int(id)]));
    await datastore.delete(keys);
}

// Start the server
const port = process.env.PORT || 8080;
app.listen(port, () => {
  console.log(`Example app listening at http://localhost:${port}`)
});

স্থাপনার বর্ণনাকারীতে স্কোপ যোগ করুন

সার্ভার পুনর্নির্মাণের আগে, https://www.googleapis.com/auth/drive.file OAuth স্কোপ অন্তর্ভুক্ত করতে অ্যাড-অন স্থাপনার বিবরণ আপডেট করুন। OAuth স্কোপের তালিকায় https://www.googleapis.com/auth/drive.file যোগ করতে deployment.json আপডেট করুন:

"oauthScopes": [
    "https://www.googleapis.com/auth/gmail.addons.execute",
    "https://www.googleapis.com/auth/calendar.addons.execute",
    "https://www.googleapis.com/auth/drive.file",
    "openid",
    "email"
]

এই ক্লাউড শেল কমান্ডটি চালিয়ে নতুন সংস্করণ আপলোড করুন:

gcloud workspace-add-ons deployments replace todo-add-on --deployment-file=deployment.json

পুনরায় স্থাপন এবং পরীক্ষা

অবশেষে, সার্ভার পুনর্নির্মাণ। ক্লাউড শেল থেকে, চালান:

gcloud builds submit

একবার সম্পূর্ণ হয়ে গেলে, Gmail খোলার পরিবর্তে, একটি বিদ্যমান Google নথি খুলুন বা doc.new খুলে একটি নতুন তৈরি করুন। একটি নতুন ডক তৈরি করলে, কিছু পাঠ্য লিখতে ভুলবেন না বা ফাইলটিকে একটি নাম দিন৷

অ্যাড-অন খুলুন। অ্যাড-অন অ্যাড-অনের নীচে একটি অনুমোদিত ফাইল অ্যাক্সেস বোতাম প্রদর্শন করে। বোতামে ক্লিক করুন, তারপর ফাইলে অ্যাক্সেস অনুমোদন করুন।

একবার অনুমোদিত হলে, সম্পাদক থাকাকালীন একটি টাস্ক যোগ করুন। কার্যটিতে একটি লেবেল রয়েছে যা নির্দেশ করে যে নথিটি সংযুক্ত রয়েছে৷ লিঙ্কটিতে ক্লিক করলে ডকুমেন্টটি একটি নতুন ট্যাবে খোলে। অবশ্যই, আপনার ইতিমধ্যে খোলা ডকুমেন্ট খোলা একটু নির্বোধ. আপনি যদি বর্তমান নথির জন্য লিঙ্কগুলি ফিল্টার করার জন্য UI-কে অপ্টিমাইজ করতে চান, সেই অতিরিক্ত ক্রেডিট বিবেচনা করুন!

8. অভিনন্দন

অভিনন্দন! আপনি ক্লাউড রান ব্যবহার করে সফলভাবে একটি Google Workpace অ্যাড-অন তৈরি এবং স্থাপন করেছেন। যদিও কোডল্যাব একটি অ্যাড-অন তৈরির জন্য অনেকগুলি মূল ধারণাকে কভার করেছে, সেখানে আরও অনেক কিছু অন্বেষণ করার আছে। নীচের সংস্থানগুলি দেখুন এবং অতিরিক্ত চার্জ এড়াতে আপনার প্রকল্পটি পরিষ্কার করতে ভুলবেন না৷

পরিষ্কার কর

আপনার অ্যাকাউন্ট থেকে অ্যাড-অন আনইনস্টল করতে, ক্লাউড শেলে, এই কমান্ডটি চালান:

gcloud workspace-add-ons deployments uninstall todo-add-on

এই টিউটোরিয়ালে ব্যবহৃত সংস্থানগুলির জন্য আপনার Google ক্লাউড প্ল্যাটফর্ম অ্যাকাউন্টে চার্জ এড়াতে:

  • ক্লাউড কনসোলে, সম্পদ পরিচালনা পৃষ্ঠাতে যান। উপরের-বাম কোণায় ক্লিক করুন, মেনুতে ক্লিক করুন মেনু আইকন > আইএএম এবং অ্যাডমিন > সম্পদ পরিচালনা করুন
  1. প্রকল্প তালিকায়, আপনার প্রকল্প নির্বাচন করুন তারপর মুছুন ক্লিক করুন।
  2. ডায়ালগে, প্রকল্প আইডি টাইপ করুন এবং তারপরে প্রকল্পটি মুছে ফেলতে শাট ডাউন ক্লিক করুন।

আরও জানুন