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

1. ভূমিকা

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

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

তুমি কি শিখবে

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

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

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

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

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

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

নতুন প্রজেক্ট বোতাম

প্রকল্প আইডি

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

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

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

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

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

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

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

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

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

ক্লাউড শেল স্বাগত বার্তা

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

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

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

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

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

যদি আপনাকে GCP API কল করার জন্য Cloud Shell কে অনুমোদন করতে বলা হয়, তাহলে Authorize এ ক্লিক করুন।

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

 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].

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

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

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

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

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

শুরু করার জন্য, আপনি একটি সহজ "হ্যালো ওয়ার্ল্ড" অ্যাড-অন তৈরি করবেন এবং এটি স্থাপন করবেন। অ্যাড-অন হল এমন ওয়েব পরিষেবা যা 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 নামে একটি 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

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

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

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

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

নিম্নলিখিত কন্টেন্ট দিয়ে deployment.json ফাইলটি তৈরি করুন। URL প্লেসহোল্ডারের পরিবর্তে deployed অ্যাপের 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 Run-এর জন্য 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/] খুলুন। ডানদিকে, চেকমার্ক আইকন সহ অ্যাড-অনটি খুঁজুন।

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

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

অনুমোদনের অনুরোধ

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

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

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

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

ডিপ্লয়মেন্ট বর্ণনাকারীতে স্কোপ যোগ করুন

ব্যবহারকারীর পরিচয় ডিফল্টভাবে পাঠানো হয় না। এটি ব্যবহারকারীর ডেটা এবং অ্যাড-অনটিকে এটি অ্যাক্সেস করার জন্য অনুমতির প্রয়োজন। সেই অনুমতি পেতে, deployment.json আপডেট করুন এবং openid যোগ করুন এবং অ্যাড-অনের প্রয়োজনীয় স্কোপের তালিকায় OAuth স্কোপগুলিকে email । 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

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

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

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

প্রকল্পে গুগল অথ লাইব্রেরি যোগ করে শুরু করুন:

npm install --save google-auth-library

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

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 খুলুন বা পুনরায় লোড করুন এবং অ্যাড-অনটি আবার খুলুন। যেহেতু স্কোপগুলি পরিবর্তিত হয়েছে, অ্যাড-অনটি পুনরায় অনুমোদনের জন্য অনুরোধ করবে। অ্যাড-অনটি আবার অনুমোদন করুন, এবং সম্পূর্ণ হয়ে গেলে অ্যাড-অনটি আপনার ইমেল ঠিকানা এবং ব্যবহারকারী আইডি প্রদর্শন করবে।

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

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

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

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

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

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

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

তারপর ডেটাস্টোর সূচীগুলি আপডেট করুন:

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 টিপে কয়েকটি কাজ যোগ করুন, তারপর সেগুলি মুছে ফেলার জন্য চেকবক্সে ক্লিক করুন।

কাজের সাথে অ্যাড-অন

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

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

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

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

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

৮. অভিনন্দন

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

পরিষ্কার করা

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

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

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

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

আরও জানুন

  • Google Workspace অ্যাড-অনগুলির ওভারভিউ
  • বাজারে বিদ্যমান অ্যাপ এবং অ্যাড-অনগুলি খুঁজুন