692 lines
16 KiB
Go
692 lines
16 KiB
Go
package handlers
|
|
|
|
import (
|
|
"amocrm_templategen_back/amo"
|
|
"amocrm_templategen_back/dal/model"
|
|
"amocrm_templategen_back/templategen"
|
|
"errors"
|
|
"fmt"
|
|
"github.com/gorilla/schema"
|
|
docTemp "github.com/opencontrol/doc-template"
|
|
"golang.org/x/oauth2"
|
|
"io/ioutil"
|
|
"net/http"
|
|
"net/url"
|
|
"strconv"
|
|
"time"
|
|
)
|
|
|
|
type ReqAmoSaveToken struct {
|
|
AccessToken string `json:"access_token" schema:"access_token"`
|
|
Code string `json:"code" schema:"code"`
|
|
ClientID string `json:"client_id" schema:"client_id"`
|
|
ExpiresIn int64 `json:"expires_in" schema:"expires_in"`
|
|
TokenType string `json:"token_type" schema:"token_type"`
|
|
RefreshToken string `json:"refresh_token" schema:"refresh_token"`
|
|
State string `json:"state" schema:"state"`
|
|
FromWidget string `json:"from_widget" schema:"from_widget"`
|
|
Referer string `json:"referer" schema:"referer"`
|
|
}
|
|
|
|
func decodeJwt(w http.ResponseWriter) {
|
|
|
|
}
|
|
|
|
func (h *Handlers) AmoSaveToken(w http.ResponseWriter, r *http.Request) {
|
|
var resp ReqAmoSaveToken
|
|
|
|
err := r.ParseForm()
|
|
if err != nil {
|
|
h.reportError(w, err, http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
err = schema.NewDecoder().Decode(&resp, r.Form)
|
|
if err != nil {
|
|
h.reportError(w, err, 500)
|
|
return
|
|
}
|
|
|
|
if resp.AccessToken == "" && resp.Code == "" {
|
|
err = errors.New("AmoErr: got empty token")
|
|
h.reportError(w, err, http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
token := &oauth2.Token{
|
|
AccessToken: resp.AccessToken,
|
|
RefreshToken: resp.RefreshToken,
|
|
TokenType: resp.TokenType,
|
|
Expiry: time.Now().Add(time.Duration(resp.ExpiresIn) * time.Second),
|
|
}
|
|
|
|
var amoClient *amo.Client
|
|
if resp.Code != "" {
|
|
amoClient = h.Amo.NewClient(resp.Referer, resp.Code)
|
|
token, err = amoClient.AuthCode()
|
|
if err != nil {
|
|
h.reportError(w, err, 500)
|
|
return
|
|
}
|
|
}
|
|
|
|
user := getJwtUser(r)
|
|
if user == nil {
|
|
h.reportError(w, ErrorUnauthorized, http.StatusUnauthorized)
|
|
return
|
|
}
|
|
|
|
amoAcc, err := amoClient.GetAccount()
|
|
|
|
if err != nil {
|
|
h.reportError(w, err, 200)
|
|
}
|
|
if amoAcc == nil {
|
|
return
|
|
} else {
|
|
fmt.Println(amoAcc)
|
|
}
|
|
|
|
// Insert/Update token in DB
|
|
_, err = h.dal.Amo.InsertOrUpdate(r.Context(), &model.Amo{
|
|
UserID: user.UserID,
|
|
AccountID: strconv.FormatInt(amoAcc.Id, 10),
|
|
AccessToken: token.AccessToken,
|
|
RefreshToken: token.RefreshToken,
|
|
Code: resp.Code,
|
|
FromWidget: resp.FromWidget,
|
|
Referer: resp.Referer,
|
|
Subdomain: amoAcc.Subdomain,
|
|
ExpiresIn: token.Expiry,
|
|
TokenType: token.TokenType,
|
|
})
|
|
|
|
if err != nil {
|
|
h.reportError(w, err, 500)
|
|
return
|
|
}
|
|
|
|
err = sendResponse(w, 200, map[string]string{"accessToken": resp.AccessToken})
|
|
if err != nil {
|
|
h.reportError(w, err, 500)
|
|
}
|
|
}
|
|
|
|
func (h *Handlers) AmoWebhook(w http.ResponseWriter, r *http.Request) {
|
|
fmt.Println(" -------------------- AMO WEBHOOK ------------------------")
|
|
//resp := map[string]interface{}{}
|
|
|
|
user := getJwtUser(r)
|
|
if user != nil {
|
|
fmt.Println("USER:", user)
|
|
}
|
|
|
|
reqBody, err := ioutil.ReadAll(r.Body)
|
|
|
|
if err != nil {
|
|
h.reportError(w, err, 200)
|
|
return
|
|
}
|
|
|
|
p, err := url.ParseQuery(string(reqBody))
|
|
if err != nil {
|
|
h.reportError(w, err, 200)
|
|
return
|
|
}
|
|
|
|
fmt.Println("RESPONSE:")
|
|
fmt.Println(p.Encode())
|
|
|
|
sendResponse(w, 200, nil)
|
|
}
|
|
|
|
func (h *Handlers) AmoWebhook2(w http.ResponseWriter, r *http.Request) {
|
|
fmt.Println(" -------------------- AMO WEBHOOK 2------------------------")
|
|
reqBody, err := ioutil.ReadAll(r.Body)
|
|
|
|
if err != nil {
|
|
h.reportError(w, err, 200)
|
|
return
|
|
}
|
|
|
|
p, err := url.ParseQuery(string(reqBody))
|
|
if err != nil {
|
|
h.reportError(w, err, 200)
|
|
return
|
|
}
|
|
|
|
leadId := p.Get("leads[status][0][id]")
|
|
subdomain := p.Get("account[subdomain]")
|
|
accId := p.Get("account[id]")
|
|
|
|
// Запрашиваем данные по аккаунту
|
|
amoData, err := h.dal.Amo.GetByAccountID(r.Context(), accId)
|
|
if err != nil {
|
|
h.reportError(w, err, 200)
|
|
return
|
|
}
|
|
|
|
amoClient := h.Amo.NewClient(subdomain+".amocrm.ru", amoData.Code)
|
|
|
|
amoClient.SetToken(&oauth2.Token{
|
|
AccessToken: amoData.AccessToken,
|
|
TokenType: amoData.TokenType,
|
|
RefreshToken: amoData.RefreshToken,
|
|
Expiry: amoData.ExpiresIn,
|
|
})
|
|
|
|
lead, err := amoClient.GetLeadById(leadId)
|
|
if err != nil {
|
|
h.reportError(w, err, 200)
|
|
return
|
|
}
|
|
|
|
if lead != nil {
|
|
// fmt.Printf("Lead RESP:%+v\r\n", lead)
|
|
} else {
|
|
fmt.Println("Lead empty")
|
|
}
|
|
|
|
dataTemplate := map[string]interface{}{}
|
|
|
|
// Добавляем Инфо Лида
|
|
for k, v := range templategen.AmoLeadFieldsToRuMap(lead) {
|
|
dataTemplate[k] = v
|
|
}
|
|
|
|
filename := fmt.Sprintf("%v", dataTemplate["Filename"])
|
|
|
|
// Добавялем инфо контактов
|
|
contacts := []amo.Contact{}
|
|
for _, data := range lead.Embedded.Contacts {
|
|
contact, err := amoClient.GetContactById(strconv.Itoa(data.Id))
|
|
if err == nil {
|
|
contacts = append(contacts, *contact)
|
|
} else {
|
|
fmt.Println("Something Wrong1:", err)
|
|
}
|
|
}
|
|
|
|
dataTemplate["Контакты"] = templategen.AmoContactsFieldsToRuMap(contacts)
|
|
|
|
// Добавляем инфо компаний
|
|
companies := []amo.Company{}
|
|
for _, data := range lead.Embedded.Companies {
|
|
company, err := amoClient.GetCompanyById(strconv.Itoa(data.Id))
|
|
if err == nil {
|
|
companies = append(companies, *company)
|
|
} else {
|
|
fmt.Println("Something Wrong2:", err)
|
|
}
|
|
}
|
|
|
|
dataTemplate["Компании"] = templategen.AmoCompaniesFieldsToRuMap(companies)
|
|
|
|
//user := getJwtUser(r)
|
|
//if user != nil {
|
|
// //
|
|
//}
|
|
|
|
fmt.Printf("dataTemplate: %+v\r\n", dataTemplate)
|
|
|
|
user, err := h.dal.User.GetByID(r.Context(), amoData.UserID)
|
|
if err != nil {
|
|
h.reportError(w, err, 200)
|
|
}
|
|
|
|
// PARSING
|
|
yaDiskData, err := h.dal.YaDisk.GetByUserID(r.Context(), user.ID)
|
|
if err != nil {
|
|
h.reportError(w, err, http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
if yaDiskData == nil {
|
|
h.reportError(w, errors.New("YaDisk not found"), 200)
|
|
return
|
|
}
|
|
|
|
token := &oauth2.Token{
|
|
AccessToken: yaDiskData.AccessToken,
|
|
TokenType: yaDiskData.TokenType,
|
|
RefreshToken: yaDiskData.RefreshToken,
|
|
Expiry: yaDiskData.ExpiresIn,
|
|
}
|
|
|
|
if !token.Valid() {
|
|
h.reportError(w, errors.New("yandex token invalid"), http.StatusForbidden)
|
|
return
|
|
}
|
|
|
|
// download file
|
|
client := h.YaDisk.NewClient(token)
|
|
|
|
res, err := client.GetResources(yaDiskData.TemplateFolder + "/" + filename)
|
|
if res == nil {
|
|
h.reportError(w, errors.New("resource in yandex disk not found"), http.StatusNotFound)
|
|
return
|
|
}
|
|
downloadFileName := fmt.Sprintf("./tmp/downloaded/%v_%v", user.ID, filename)
|
|
|
|
err = templategen.DownloadDocument(downloadFileName, res.File)
|
|
if err != nil {
|
|
h.reportError(w, err, 200)
|
|
return
|
|
}
|
|
|
|
// parsing file
|
|
dc, err := docTemp.GetTemplate(downloadFileName)
|
|
if err != nil {
|
|
h.reportError(w, err, 200)
|
|
return
|
|
}
|
|
|
|
dc.Parse()
|
|
|
|
saveFilename := fmt.Sprintf("%v_%v_%v", user.ID, time.Now().Unix(), filename)
|
|
err = dc.Execute("./tmp/parsed/"+saveFilename, dataTemplate)
|
|
if err != nil {
|
|
h.reportError(w, err, 200)
|
|
return
|
|
}
|
|
|
|
// Send to Yandex Disk
|
|
|
|
err = client.ResourcesUpload(yaDiskData.SaveFolder+"/"+saveFilename, "https://solweb.site/tmp/parsed/"+saveFilename)
|
|
if err != nil {
|
|
h.reportError(w, err, 200)
|
|
return
|
|
}
|
|
|
|
//err := decodePost(&resp, r)
|
|
//if err != nil {
|
|
// h.reportError(w, err, 200)
|
|
//}
|
|
|
|
//err := r.ParseForm()
|
|
//if err != nil {
|
|
// h.reportError(w, err, 200)
|
|
//}
|
|
//
|
|
//err = schema.NewDecoder().Decode(&resp, r.Form)
|
|
//if err != nil {
|
|
// h.reportError(w, err, 200)
|
|
//}
|
|
|
|
//fmt.Println("RESPONSE:", r.PostForm.Encode())
|
|
sendResponse(w, 200, nil)
|
|
}
|
|
|
|
type RespAmoSettingsGetData struct {
|
|
Service struct {
|
|
IsAuth bool `json:"is_auth"`
|
|
IsActivated bool `json:"isActivated"`
|
|
} `json:"service"`
|
|
Google struct {
|
|
IsAuth bool `json:"is_auth"`
|
|
AuthUrl string `json:"auth_url,omitempty"`
|
|
} `json:"google"`
|
|
Yandex struct {
|
|
IsAuth bool `json:"is_auth"`
|
|
AuthUrl string `json:"auth_url,omitempty"`
|
|
} `json:"yandex"`
|
|
}
|
|
|
|
func (h *Handlers) AmoSettingsGetData(w http.ResponseWriter, r *http.Request) {
|
|
//
|
|
//reqBody, err := ioutil.ReadAll(r.Body)
|
|
//if err != nil {
|
|
// h.reportError(w, err, 200)
|
|
// return
|
|
//}
|
|
//
|
|
//fmt.Println(reqBody)
|
|
|
|
amoData := getAmoByJwt(r)
|
|
|
|
if amoData == nil {
|
|
h.reportError(w, errors.New("amo account not found"), http.StatusUnauthorized)
|
|
return
|
|
}
|
|
|
|
user, err := h.dal.User.GetByID(r.Context(), amoData.UserID)
|
|
if err != nil {
|
|
h.reportError(w, err, http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
if user == nil {
|
|
h.reportError(w, errors.New("user not found"), http.StatusUnauthorized)
|
|
return
|
|
}
|
|
|
|
req := RespAmoSettingsGetData{}
|
|
req.Service.IsAuth = true
|
|
|
|
yandexData, err := h.dal.YaDisk.GetByUserID(r.Context(), user.ID)
|
|
if err != nil {
|
|
h.reportError(w, err, http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
if yandexData != nil {
|
|
req.Yandex.IsAuth = time.Now().Before(yandexData.ExpiresIn) && yandexData.AccessToken != ""
|
|
}
|
|
|
|
if !req.Yandex.IsAuth {
|
|
req.Yandex.AuthUrl = h.YaDisk.GenerateOAuthUrl()
|
|
}
|
|
|
|
googleData, err := h.dal.GDisk.GetByUserID(r.Context(), user.ID)
|
|
if err != nil {
|
|
h.reportError(w, err, http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
if googleData != nil {
|
|
req.Google.IsAuth = time.Now().Before(googleData.ExpiresIn) && googleData.AccessToken != ""
|
|
}
|
|
|
|
if !req.Google.IsAuth {
|
|
req.Google.AuthUrl = h.GDisk.GenerateOAuthUrl()
|
|
}
|
|
|
|
sendResponse(w, 200, req)
|
|
}
|
|
|
|
type ReqAmoGetTemplate struct {
|
|
LeadId int64 `json:"lead_id"`
|
|
TemplateId string `json:"template_id"`
|
|
}
|
|
|
|
func (h *Handlers) AmoGetTemplate(w http.ResponseWriter, r *http.Request) {
|
|
var req ReqAmoGetTemplate
|
|
|
|
amoData := getAmoByJwt(r)
|
|
|
|
if amoData == nil {
|
|
h.reportError(w, errors.New("amo account not found"), http.StatusUnauthorized)
|
|
return
|
|
}
|
|
|
|
err := decodePost(&req, r)
|
|
if err != nil {
|
|
h.reportError(w, err, http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
if req.TemplateId == "" && strconv.FormatInt(req.LeadId, 10) == "" {
|
|
h.reportError(w, err, http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
var template *model.Template
|
|
|
|
if req.TemplateId != "" {
|
|
template, err = h.dal.Template.GetByID(r.Context(), req.TemplateId)
|
|
if err != nil {
|
|
h.reportError(w, err, http.StatusInternalServerError)
|
|
return
|
|
}
|
|
}
|
|
|
|
if strconv.FormatInt(req.LeadId, 10) != "" {
|
|
template, err = h.dal.Template.GetByLeadId(r.Context(), strconv.FormatInt(req.LeadId, 10))
|
|
if err != nil {
|
|
h.reportError(w, err, http.StatusInternalServerError)
|
|
return
|
|
}
|
|
}
|
|
|
|
if template == nil {
|
|
templateId, err := h.dal.Template.Insert(r.Context(), &model.Template{
|
|
UserID: amoData.UserID,
|
|
LeadId: strconv.FormatInt(req.LeadId, 10),
|
|
})
|
|
if err != nil {
|
|
h.reportError(w, err, http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
template = &model.Template{ID: templateId}
|
|
|
|
}
|
|
|
|
sendResponse(w, 200, template)
|
|
}
|
|
|
|
type ReqAmoSetTemplate struct {
|
|
LeadId int64 `json:"lead_id"`
|
|
TemplateId string `json:"template_id"`
|
|
Filename string `json:"filename"`
|
|
FileID string `json:"file_id"` // for Google Drive
|
|
Storage string `json:"storage"` // google or yandex
|
|
}
|
|
|
|
type RespAmoSetTemplate struct {
|
|
TemplateId string `json:"template_id"`
|
|
}
|
|
|
|
func (h *Handlers) AmoSetTemplate(w http.ResponseWriter, r *http.Request) {
|
|
var req ReqAmoSetTemplate
|
|
|
|
amoData := getAmoByJwt(r)
|
|
|
|
if amoData == nil {
|
|
h.reportError(w, errors.New("amo account not found"), http.StatusUnauthorized)
|
|
return
|
|
}
|
|
|
|
err := decodePost(&req, r)
|
|
if err != nil {
|
|
h.reportError(w, err, http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
fmt.Println(req)
|
|
|
|
if strconv.FormatInt(req.LeadId, 10) == "" || req.Storage == "" {
|
|
h.reportError(w, err, http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
if req.Filename == "" && req.FileID == "" {
|
|
h.reportError(w, err, http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
// Search/update template
|
|
|
|
var template *model.Template
|
|
|
|
update := &model.Template{
|
|
ID: req.TemplateId,
|
|
UserID: amoData.UserID,
|
|
LeadId: strconv.FormatInt(req.LeadId, 10),
|
|
Name: "Sample",
|
|
Filename: req.Filename,
|
|
Storage: req.Storage,
|
|
FileID: req.FileID,
|
|
IsDeleted: false,
|
|
}
|
|
|
|
templateId := ""
|
|
if req.TemplateId == "" {
|
|
template, err = h.dal.Template.GetByLeadId(r.Context(), strconv.FormatInt(req.LeadId, 10))
|
|
fmt.Println(0, template)
|
|
|
|
if template != nil {
|
|
fmt.Println("1", update)
|
|
err = h.dal.Template.UpdateByLeadID(r.Context(), update)
|
|
} else {
|
|
fmt.Println("2", update)
|
|
templateId, err = h.dal.Template.Insert(r.Context(), update)
|
|
}
|
|
} else {
|
|
err = h.dal.Template.UpdateByID(r.Context(), update)
|
|
}
|
|
|
|
if err != nil {
|
|
h.reportError(w, err, http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
sendResponse(w, 200, RespAmoSetTemplate{templateId})
|
|
}
|
|
|
|
type ReqAmoGenerateDoc struct {
|
|
LeadId int64 `json:"lead_id"`
|
|
Filename string `json:"filename"`
|
|
}
|
|
|
|
func (h *Handlers) AmoGenerateDoc(w http.ResponseWriter, r *http.Request) {
|
|
var req ReqAmoGenerateDoc
|
|
|
|
amoData := getAmoByJwt(r)
|
|
|
|
if amoData == nil {
|
|
h.reportError(w, errors.New("amo account not found"), http.StatusUnauthorized)
|
|
return
|
|
}
|
|
|
|
err := decodePost(&req, r)
|
|
if err != nil {
|
|
h.reportError(w, err, http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
leadId := strconv.FormatInt(req.LeadId, 10)
|
|
|
|
if leadId == "" {
|
|
h.reportError(w, err, http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
template, err := h.dal.Template.GetByLeadId(r.Context(), leadId)
|
|
if err != nil {
|
|
h.reportError(w, err, http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
if template == nil {
|
|
h.reportError(w, err, http.StatusNotFound)
|
|
return
|
|
}
|
|
|
|
// Получить данные по лиду
|
|
amoClient := h.Amo.NewClient(amoData.Subdomain+".amocrm.ru", amoData.Code)
|
|
|
|
amoClient.SetToken(&oauth2.Token{
|
|
AccessToken: amoData.AccessToken,
|
|
TokenType: amoData.TokenType,
|
|
RefreshToken: amoData.RefreshToken,
|
|
Expiry: amoData.ExpiresIn,
|
|
})
|
|
|
|
lead, err := amoClient.GetLeadById(leadId)
|
|
if err != nil {
|
|
h.reportError(w, err, 200)
|
|
return
|
|
}
|
|
|
|
if lead.Id == 0 {
|
|
fmt.Println("Lead empty")
|
|
}
|
|
|
|
dataTemplate := map[string]interface{}{}
|
|
|
|
// Добавляем Инфо Лида
|
|
for k, v := range templategen.AmoLeadFieldsToRuMap(lead) {
|
|
dataTemplate[k] = v
|
|
}
|
|
|
|
filename := fmt.Sprintf("%v_%v_%v.docx", req.Filename, amoData.UserID, time.Now().Unix())
|
|
|
|
// Добавялем инфо контактов
|
|
contacts := []amo.Contact{}
|
|
for _, data := range lead.Embedded.Contacts {
|
|
contact, err := amoClient.GetContactById(strconv.Itoa(data.Id))
|
|
if err == nil {
|
|
contacts = append(contacts, *contact)
|
|
} else {
|
|
fmt.Println("Something Wrong1:", err)
|
|
}
|
|
}
|
|
|
|
dataTemplate["Контакты"] = templategen.AmoContactsFieldsToRuMap(contacts)
|
|
|
|
// Добавляем инфо компаний
|
|
companies := []amo.Company{}
|
|
for _, data := range lead.Embedded.Companies {
|
|
company, err := amoClient.GetCompanyById(strconv.Itoa(data.Id))
|
|
if err == nil {
|
|
companies = append(companies, *company)
|
|
} else {
|
|
fmt.Println("Something Wrong2:", err)
|
|
}
|
|
}
|
|
|
|
dataTemplate["Компании"] = templategen.AmoCompaniesFieldsToRuMap(companies)
|
|
|
|
fmt.Printf("TemplateInfo:%+v\r\n", dataTemplate)
|
|
|
|
// Скачать файл
|
|
|
|
// Google OAuth
|
|
googleData, err := h.dal.GDisk.GetByUserID(r.Context(), amoData.UserID)
|
|
if err != nil {
|
|
h.reportError(w, err, http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
if googleData == nil {
|
|
h.reportError(w, errors.New("google data not found"), http.StatusForbidden)
|
|
return
|
|
}
|
|
|
|
if googleData.Token() == nil {
|
|
h.reportError(w, errors.New("invalid google token"), http.StatusForbidden)
|
|
return
|
|
}
|
|
|
|
googleClient, err := h.GDisk.NewClient(r.Context(), googleData.Token())
|
|
if err != nil {
|
|
h.reportError(w, err, http.StatusInternalServerError)
|
|
return
|
|
}
|
|
|
|
err = googleClient.DownloadFile("./tmp/downloaded/"+filename, template.FileID)
|
|
if err != nil {
|
|
h.reportError(w, err, 500)
|
|
return
|
|
}
|
|
|
|
// Сгенерировать
|
|
// parsing file
|
|
dc, err := docTemp.GetTemplate("./tmp/downloaded/" + filename)
|
|
if err != nil {
|
|
h.reportError(w, err, 500)
|
|
return
|
|
}
|
|
|
|
dc.Parse()
|
|
|
|
err = dc.Execute("./tmp/parsed/"+filename, dataTemplate)
|
|
if err != nil {
|
|
h.reportError(w, err, 500)
|
|
return
|
|
}
|
|
|
|
// Загрузить
|
|
// application/vnd.openxmlformats-officedocument.wordprocessingml.document or application/msword
|
|
err = googleClient.UploadFile("./tmp/parsed/"+filename, "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
googleData.SaveFolderID)
|
|
if err != nil {
|
|
h.reportError(w, err, 500)
|
|
return
|
|
}
|
|
|
|
fmt.Println("Parsing Done")
|
|
|
|
}
|