go-ayo/common을 gocommon으로 분리

This commit is contained in:
2023-05-24 15:10:15 +09:00
commit e95efa06a6
21 changed files with 4389 additions and 0 deletions

456
azure/bot.go Normal file
View File

@ -0,0 +1,456 @@
package azure
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"strconv"
"strings"
"time"
)
type ChannelAccount struct {
Id string `json:"id"`
Name string `json:"name,omitempty"`
AadObjectId string `json:"aadObjectId,omitempty"`
Role string `json:"role,omitempty"`
}
type AttachmentData struct {
Name string `json:"name,omitempty"`
OriginalBase64 string `json:"originalBase64,omitempty"`
ThumbnailBase64 string `json:"thumbnailBase64,omitempty"`
Type string `json:"type,omitempty"`
}
type MessageActivity struct {
Type string `json:"type,omitempty"`
Timestamp time.Time `json:"timestamp,omitempty"`
LocalTimestamp time.Time `json:"localTimestamp,omitempty"`
Id string `json:"id"`
ChannelId string `json:"channelId,omitempty"`
ServiceUrl string `json:"serviceUrl,omitempty"`
Message string
Mentions []string
From ChannelAccount `json:"from,omitempty"`
Conversation *struct {
ConversationType string `json:"conversationType,omitempty"`
TenantId string `json:"tenantId,omitempty"`
Id string `json:"id"`
Name string `json:"name"`
} `json:"conversation,omitempty"`
Recipient *struct {
Name string `json:"name,omitempty"`
Id string `json:"id"`
} `json:"recipient,omitempty"`
Entities []interface{} `json:"entities,omitempty"`
ChannelData *struct {
TeamsChannelId string `json:"teamsChannelId,omitempty"`
TeamsTeamId string `json:"teamsTeamId,omitempty"`
Channel *struct {
Id string `json:"id"`
} `json:"channel,omitempty"`
Team *struct {
Id string `json:"id"`
} `json:"team,omitempty"`
Tenant *struct {
Id string `json:"id,omitempty"`
} `json:"tenant,omitempty"`
} `json:"channelData,omitempty"`
ReplyToId string `json:"replyToId,omitempty"`
Value map[string]string `json:"value,omitempty"`
Locale string `json:"Locale,omitempty"`
LocalTimezone string `json:"LocalTimezone,omitempty"`
}
type AdaptiveCard struct {
Body []interface{}
Actions []interface{}
}
type tempMessageActivity struct {
MessageActivity
RawText string `json:"text,omitempty"`
}
var botFrameworkAuth = accessToken{}
func ParseMessageActivity(src []byte) (MessageActivity, error) {
var message tempMessageActivity
err := json.Unmarshal(src, &message)
if err != nil {
return MessageActivity{}, err
}
for len(message.RawText) > 0 {
s := strings.Index(message.RawText, "<at>")
if s < 0 {
break
}
message.Message += message.RawText[:s]
message.RawText = message.RawText[s+4:]
e := strings.Index(message.RawText, "</at>")
if e < 0 {
break
}
mention := message.RawText[:e]
message.RawText = message.RawText[e+5:]
message.Mentions = append(message.Mentions, mention)
}
message.Message += strings.Trim(message.RawText, " ")
message.Message = strings.TrimSpace(message.Message)
if !strings.HasSuffix(message.ServiceUrl, "/") {
message.ServiceUrl += "/"
}
return message.MessageActivity, nil
}
func sendRequireMessage(requrl string, method string, stream io.Reader) (string, error) {
req, err := http.NewRequest(method, requrl, stream)
if err != nil {
return "", err
}
auth, err := botFrameworkAuth.getAuthoizationToken()
if err != nil {
return "", err
}
req.Header.Set("Authorization", auth)
req.Header.Set("Content-type", "application/json")
resp, err := http.DefaultClient.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()
bt, _ := io.ReadAll(resp.Body)
var resultdoc map[string]interface{}
json.Unmarshal(bt, &resultdoc)
if len(resultdoc) > 0 {
if idraw, ok := resultdoc["id"]; ok {
return idraw.(string), nil
}
}
return "", err
}
func putMessage(requrl string, stream io.Reader) (string, error) {
return sendRequireMessage(requrl, "PUT", stream)
}
func postMessage(requrl string, stream io.Reader) (string, error) {
return sendRequireMessage(requrl, "POST", stream)
}
func unescapeUnicodeCharactersInJSON(src []byte) (string, error) {
str, err := strconv.Unquote(strings.Replace(strconv.Quote(string(src)), `\\u`, `\u`, -1))
if err != nil {
return "", err
}
return str, nil
}
func (m *MessageActivity) SendStreamOnChannel(reader io.Reader) (string, error) {
serviceUrl := m.ServiceUrl
// mention 없이 여기 왔으면 private 채팅이다. reply를 해야함
var conversationId string
if m.ChannelData.Channel == nil {
conversationId = m.Conversation.Id
} else {
conversationId = m.ChannelData.Channel.Id
}
requrl := fmt.Sprintf("%sv3/conversations/%s/activities", serviceUrl, conversationId)
return postMessage(requrl, reader)
}
func (m *MessageActivity) UploadFile(attachment AttachmentData) (string, error) {
// POST /v3/conversations/{conversationId}/attachments
// Request body An AttachmentData object.
// Returns A ResourceResponse object. The id property specifies the attachment ID that can be used with the Get Attachment Info operation and the Get Attachment operation.
serviceUrl := m.ServiceUrl
conversationId := m.Conversation.Id
bt, _ := json.Marshal(attachment)
requrl := fmt.Sprintf("%sv3/conversations/%s/attachments", serviceUrl, conversationId)
return postMessage(requrl, bytes.NewReader([]byte(bt)))
}
func makeAttachmentDocs(attachments []AdaptiveCard) []interface{} {
var attachconv []interface{}
for _, attachment := range attachments {
attachconv = append(attachconv, map[string]interface{}{
"contentType": "application/vnd.microsoft.card.adaptive",
"content": map[string]interface{}{
"$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
"type": "AdaptiveCard",
"version": "1.3",
"body": attachment.Body,
},
})
}
return attachconv
}
// func (m *MessageActivity) update(activityId string, message string, attachments ...AdaptiveCard) (string, error) {
// serviceUrl := m.ServiceUrl
// conversationId := m.Conversation.Id
// attachconv := makeAttachmentDocs(attachments)
// activity := map[string]interface{}{
// "type": "message",
// "text": strings.ReplaceAll(message, `"`, `\"`),
// }
// if len(attachconv) > 0 {
// activity["attachments"] = attachconv
// }
// reqdoc, _ := json.Marshal(activity)
// unq, _ := unescapeUnicodeCharactersInJSON(reqdoc)
// requrl := fmt.Sprintf("%sv3/conversations/%s/activities/%s", serviceUrl, conversationId, activityId)
// return putMessage(requrl, bytes.NewReader([]byte(unq)))
// }
// func (m *MessageActivity) reply(message string, mention bool, attachments ...AdaptiveCard) (string, error) {
// serviceUrl := m.ServiceUrl
// conversationId := m.Conversation.Id
// attachconv := makeAttachmentDocs(attachments)
// var activity map[string]interface{}
// if mention {
// mentionText := fmt.Sprintf(`<at>%s</at>`, m.From.Name)
// activity = map[string]interface{}{
// "type": "message",
// "text": mentionText + " " + strings.ReplaceAll(message, `"`, `\"`),
// "entities": []interface{}{
// map[string]interface{}{
// "mentioned": m.From,
// "text": mentionText,
// "type": "mention",
// },
// },
// }
// } else {
// activity = map[string]interface{}{
// "type": "message",
// "text": strings.ReplaceAll(message, `"`, `\"`),
// }
// }
// activity["replyToId"] = m.Id
// if len(attachconv) > 0 {
// activity["attachments"] = attachconv
// }
// reqdoc, _ := json.Marshal(activity)
// unq, _ := unescapeUnicodeCharactersInJSON(reqdoc)
// requrl := fmt.Sprintf("%sv3/conversations/%s/activities", serviceUrl, conversationId)
// return postMessage(requrl, bytes.NewReader([]byte(unq)))
// }
// func (m *MessageActivity) ReplyWithMentionf(format string, val ...interface{}) (string, error) {
// return m.ReplyWithMention(fmt.Sprintf(format, val...))
// }
// func (m *MessageActivity) ReplyWithMention(message string, attachments ...AdaptiveCard) (string, error) {
// return m.reply(message, true, attachments...)
// }
// func (m *MessageActivity) Reply(message string, attachments ...AdaptiveCard) (string, error) {
// return m.reply(message, false, attachments...)
// }
// func (m *MessageActivity) Replyf(format string, val ...interface{}) (string, error) {
// return m.Reply(fmt.Sprintf(format, val...))
// }
// func (m *MessageActivity) Update(activityId string, message string, attachments ...AdaptiveCard) (string, error) {
// return m.update(activityId, message, attachments...)
// }
// func (m *MessageActivity) Updatef(activityId string, format string, val ...interface{}) (string, error) {
// return m.Update(activityId, fmt.Sprintf(format, val...))
// }
type MessageWrap struct {
*MessageActivity
Prefix string
Mention bool
Attachments []AdaptiveCard
}
func (m *MessageActivity) MakeWrap() *MessageWrap {
return &MessageWrap{MessageActivity: m}
}
func (m *MessageWrap) WithPrefix(prefix string) *MessageWrap {
m.Prefix = prefix
return m
}
func (m *MessageWrap) WithMention() *MessageWrap {
m.Mention = true
return m
}
func (m *MessageWrap) WithAttachments(attachments ...AdaptiveCard) *MessageWrap {
m.Attachments = attachments
return m
}
func (m *MessageWrap) Reply(text string) (string, error) {
serviceUrl := m.ServiceUrl
conversationId := m.Conversation.Id
attachconv := makeAttachmentDocs(m.Attachments)
var activity map[string]interface{}
if m.Mention {
var mentionText string
if len(m.Prefix) > 0 {
mentionText = fmt.Sprintf(`**[%s]** <at>%s</at>`, m.Prefix, m.From.Name)
} else {
mentionText = fmt.Sprintf(`<at>%s</at>`, m.From.Name)
}
activity = map[string]interface{}{
"type": "message",
"text": mentionText + " " + strings.ReplaceAll(text, `"`, `\"`),
"entities": []interface{}{
map[string]interface{}{
"mentioned": m.From,
"text": mentionText,
"type": "mention",
},
},
}
} else {
if len(m.Prefix) > 0 {
text = fmt.Sprintf("**[%s]** %s", m.Prefix, text)
}
activity = map[string]interface{}{
"type": "message",
"text": strings.ReplaceAll(text, `"`, `\"`),
}
}
activity["replyToId"] = m.Id
if len(attachconv) > 0 {
activity["attachments"] = attachconv
}
activity["from"] = m.Recipient
activity["channelId"] = m.ChannelId
reqdoc, _ := json.Marshal(activity)
unq, _ := unescapeUnicodeCharactersInJSON(reqdoc)
requrl := fmt.Sprintf("%sv3/conversations/%s/activities", serviceUrl, conversationId)
return postMessage(requrl, bytes.NewReader([]byte(unq)))
}
func (m *MessageWrap) Replyf(format string, args ...interface{}) (string, error) {
return m.Reply(fmt.Sprintf(format, args...))
}
func (m *MessageActivity) Update(reader io.Reader) {
serviceUrl := m.ServiceUrl
conversationId := m.Conversation.Id
activityId := m.ReplyToId
requrl := fmt.Sprintf("%sv3/conversations/%s/activities/%s", serviceUrl, conversationId, activityId)
putMessage(requrl, reader)
}
func (m *MessageWrap) Update(activityId string, text string) (string, error) {
serviceUrl := m.ServiceUrl
conversationId := m.Conversation.Id
if len(m.Prefix) > 0 {
text = fmt.Sprintf("**[%s]** %s", m.Prefix, text)
}
attachconv := makeAttachmentDocs(m.Attachments)
activity := map[string]interface{}{
"type": "message",
"text": strings.ReplaceAll(text, `"`, `\"`),
}
if len(attachconv) > 0 {
activity["attachments"] = attachconv
}
reqdoc, _ := json.Marshal(activity)
unq, _ := unescapeUnicodeCharactersInJSON(reqdoc)
requrl := fmt.Sprintf("%sv3/conversations/%s/activities/%s", serviceUrl, conversationId, activityId)
return putMessage(requrl, bytes.NewReader([]byte(unq)))
}
func (m *MessageWrap) Updatef(activityId string, format string, args ...interface{}) (string, error) {
return m.Update(activityId, fmt.Sprintf(format, args...))
}
type MessageReplyCache struct {
sourceActivity *MessageActivity
ReplyWrap *MessageWrap
Replyaid string
}
func (c *MessageReplyCache) Serialize() string {
bt, err := json.Marshal(c)
if err != nil {
return ""
}
linkfilename := fmt.Sprintf("updatelink_%s.json", time.Now().Format("2006-01-02T15-04-05"))
if os.WriteFile(linkfilename, bt, 0666) == nil {
return linkfilename
}
return ""
}
func DeserializeMessageReplyCache(filename string) *MessageReplyCache {
bt, err := os.ReadFile(filename)
if err != nil {
return nil
}
var out MessageReplyCache
if json.Unmarshal(bt, &out) == nil {
return &out
}
return nil
}
func MakeMessageReplyCache(src *MessageActivity) MessageReplyCache {
return MessageReplyCache{
sourceActivity: src,
}
}
func (ch *MessageReplyCache) SourceActivity() *MessageActivity {
return ch.sourceActivity
}
func (ch *MessageReplyCache) Reply(text string) {
if ch.ReplyWrap == nil {
hostname, _ := os.Hostname()
ch.ReplyWrap = ch.sourceActivity.MakeWrap().WithPrefix(hostname)
}
if len(ch.Replyaid) == 0 {
ch.Replyaid, _ = ch.ReplyWrap.Reply(text)
} else {
ch.ReplyWrap.Update(ch.Replyaid, text)
}
}
func (ch *MessageReplyCache) Replyf(format string, args ...interface{}) {
ch.Reply(fmt.Sprintf(format, args...))
}

199
azure/func.go Normal file
View File

@ -0,0 +1,199 @@
package azure
import (
"crypto/rsa"
"encoding/base64"
"encoding/binary"
"encoding/json"
"errors"
"io"
"math"
"math/big"
"strconv"
"net/http"
"net/url"
"os"
"strings"
"sync"
"time"
"github.com/golang-jwt/jwt"
)
type accessToken struct {
sync.Mutex
typetoken string
expireAt time.Time
url string
values url.Values
}
var jwkCache struct {
headerlock sync.Mutex
typetoken string
expireAt time.Time
pks map[string]*rsa.PublicKey
}
func microsoftAppId() string {
val := os.Getenv("MICROSOFT_APP_ID")
if len(val) == 0 {
val = "b5367590-5a94-4df3-bca0-ecd4b693ddf0"
}
return val
}
func microsoftAppPassword() string {
val := os.Getenv("MICROSOFT_APP_PASSWORD")
if len(val) == 0 {
val = "~VG1cf2-~5Fw3Wz9_4.A.XxpZPO8BwJ36y"
}
return val
}
func getOpenIDConfiguration(x5t string) (*rsa.PublicKey, error) {
// https://docs.microsoft.com/ko-kr/azure/bot-service/rest-api/bot-framework-rest-connector-authentication?view=azure-bot-service-4.0
jwkCache.headerlock.Lock()
defer jwkCache.headerlock.Unlock()
if time.Now().After(jwkCache.expireAt) {
resp, err := http.Get("https://login.botframework.com/v1/.well-known/openidconfiguration")
if err != nil {
return nil, err
}
defer resp.Body.Close()
bt, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
var doc map[string]interface{}
if err = json.Unmarshal(bt, &doc); err != nil {
return nil, err
}
url := doc["jwks_uri"].(string)
resp, err = http.Get(url)
if err != nil {
return nil, err
}
defer resp.Body.Close()
bt, err = io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
if err = json.Unmarshal(bt, &doc); err != nil {
return nil, err
}
keys := doc["keys"].([]interface{})
newPks := make(map[string]*rsa.PublicKey)
for _, key := range keys {
keydoc := key.(map[string]interface{})
x5t := keydoc["x5t"].(string)
eb := make([]byte, 4)
nb, _ := base64.RawURLEncoding.DecodeString(keydoc["n"].(string))
base64.RawURLEncoding.Decode(eb, []byte(keydoc["e"].(string)))
n := big.NewInt(0).SetBytes(nb)
e := binary.LittleEndian.Uint32(eb)
pk := &rsa.PublicKey{
N: n,
E: int(e),
}
newPks[x5t] = pk
}
jwkCache.expireAt = time.Now().Add(24 * time.Hour)
jwkCache.pks = newPks
}
return jwkCache.pks[x5t], nil
}
func VerifyJWT(header string) error {
if !strings.HasPrefix(header, "Bearer ") {
return errors.New("invalid token")
}
tokenString := header[7:]
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
return getOpenIDConfiguration(token.Header["x5t"].(string))
})
if err != nil {
return err
}
if claims, ok := token.Claims.(jwt.MapClaims); ok && token.Valid {
if claims["iss"].(string) != "https://api.botframework.com" {
return errors.New("issuer is not valid")
}
if claims["aud"].(string) != microsoftAppId() {
return errors.New("audience is not valid")
}
expireAt := int64(claims["exp"].(float64))
if math.Abs(float64((expireAt-time.Now().UTC().Unix())/int64(time.Second))) >= 300 {
return errors.New("token expired")
}
} else {
return errors.New("VerifyJWT token claims failed")
}
return nil
}
func (at *accessToken) getAuthoizationToken() (string, error) {
at.Lock()
defer at.Unlock()
if len(at.url) == 0 {
at.url = "https://login.microsoftonline.com/botframework.com/oauth2/v2.0/token"
at.values = url.Values{
"grant_type": {"client_credentials"},
"client_id": {microsoftAppId()},
"scope": {"https://api.botframework.com/.default"},
"client_secret": {microsoftAppPassword()},
}
}
if time.Now().After(at.expireAt) {
resp, err := http.PostForm(at.url, at.values)
if err != nil {
return "", err
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
var doc map[string]interface{}
err = json.Unmarshal(body, &doc)
if err != nil {
return "", err
}
if v, ok := doc["error"]; ok {
if desc, ok := doc["error_description"]; ok {
return "", errors.New(desc.(string))
}
return "", errors.New(v.(string))
}
tokenType := doc["token_type"].(string)
token := doc["access_token"].(string)
expin := doc["expires_in"]
var tokenDur int
switch expin := expin.(type) {
case float64:
tokenDur = int(expin)
case string:
tokenDur, _ = strconv.Atoi(expin)
}
at.typetoken = tokenType + " " + token
at.expireAt = time.Now().Add(time.Duration(tokenDur) * time.Second)
}
return at.typetoken, nil
}

356
azure/graph.go Normal file
View File

@ -0,0 +1,356 @@
package azure
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"os"
"path"
"time"
)
type Graph struct {
*accessToken
tenantId string
groupId string
}
func NewGraphByEnv() *Graph {
tenantId := os.Getenv("GRAPH_TENANT_ID")
groupId := os.Getenv("GRAPH_GROUP_ID")
return NewGraph(tenantId, groupId)
}
func NewGraph(tenantId string, groupId string) *Graph {
return &Graph{
accessToken: &accessToken{
url: fmt.Sprintf("https://login.microsoftonline.com/%s/oauth2/v2.0/token", tenantId),
values: url.Values{
"grant_type": {"client_credentials"},
"client_id": {microsoftAppId()},
"scope": {"https://graph.microsoft.com/.default"},
"client_secret": {microsoftAppPassword()},
},
},
tenantId: tenantId,
groupId: groupId,
}
}
func (s Graph) GroupId() string {
return s.groupId
}
var errGroupIdMissing = errors.New("GRAPH_GROUP_ID is missing")
func (s Graph) ReadFile(relativePath string) (io.ReadCloser, error) {
if len(s.groupId) == 0 {
return nil, errGroupIdMissing
}
token, err := s.getAuthoizationToken()
if err != nil {
return nil, err
}
requrl := fmt.Sprintf("https://graph.microsoft.com/v1.0/groups/%s/drive/root:/%s:/content", s.groupId, relativePath)
req, _ := http.NewRequest("GET", requrl, nil)
req.Header.Add("Authorization", token)
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
if resp.StatusCode != http.StatusOK {
resp.Body.Close()
return nil, errors.New(resp.Status)
}
return resp.Body, nil
}
func (s Graph) GetWebURLOfFile(relativePath string) (string, error) {
if len(s.groupId) == 0 {
return "", errGroupIdMissing
}
token, err := s.getAuthoizationToken()
if err != nil {
return "", err
}
requrl := fmt.Sprintf("https://graph.microsoft.com/v1.0/groups/%s/drive/root:/%s:", s.groupId, relativePath)
req, _ := http.NewRequest("GET", requrl, nil)
req.Header.Add("Authorization", token)
resp, err := http.DefaultClient.Do(req)
if err != nil {
return "", err
}
defer func() {
io.Copy(io.Discard, resp.Body)
resp.Body.Close()
}()
itemdoc, _ := io.ReadAll(resp.Body)
var item map[string]interface{}
json.Unmarshal(itemdoc, &item)
url, ok := item["webUrl"]
if ok {
return url.(string), nil
}
return "", nil
}
func (s Graph) DownloadFile(relativePath string, outputFile string) error {
body, err := s.ReadFile(relativePath)
if err != nil {
return err
}
defer body.Close()
// Create the file
out, err := os.Create(outputFile)
if err != nil {
return err
}
defer out.Close()
// Write the body to file
_, err = io.Copy(out, body)
return err
}
func (s Graph) UploadStream(relativePath string, reader io.Reader) (string, error) {
if len(s.groupId) == 0 {
return "", errGroupIdMissing
}
token, err := s.getAuthoizationToken()
if err != nil {
return "", err
}
requrl := fmt.Sprintf("https://graph.microsoft.com/v1.0/groups/%s/drive/root:/%s:/content", s.groupId, relativePath)
req, _ := http.NewRequest("PUT", requrl, reader)
req.Header.Add("Authorization", token)
req.Header.Add("Content-Type", "text/plain")
resp, err := http.DefaultClient.Do(req)
if err != nil {
return "", err
}
defer func() {
io.Copy(io.Discard, resp.Body)
resp.Body.Close()
}()
body, _ := io.ReadAll(resp.Body)
unquot, _ := unescapeUnicodeCharactersInJSON(body)
var drive map[string]interface{}
err = json.Unmarshal([]byte(unquot), &drive)
if err != nil {
return "", err
}
if errnode, ok := drive["error"]; ok {
errmsg := errnode.(map[string]interface{})["message"].(string)
return "", errors.New(errmsg)
}
return drive["webUrl"].(string), nil
}
func (s Graph) UploadBytes(relativePath string, content []byte) (string, error) {
return s.UploadStream(relativePath, bytes.NewReader(content))
}
func (s Graph) GetChannelFilesFolderName(channel string) (string, error) {
if len(s.groupId) == 0 {
return "", errGroupIdMissing
}
token, err := s.getAuthoizationToken()
if err != nil {
return "", err
}
requrl := fmt.Sprintf("https://graph.microsoft.com/v1.0/teams/%s/channels/%s/filesFolder", s.groupId, channel)
req, _ := http.NewRequest("GET", requrl, nil)
req.Header.Add("Authorization", token)
req.Header.Add("Accept", "application/json")
resp, err := http.DefaultClient.Do(req)
if err != nil {
return "", err
}
defer func() {
io.Copy(io.Discard, resp.Body)
resp.Body.Close()
}()
body, _ := io.ReadAll(resp.Body)
unquot, _ := unescapeUnicodeCharactersInJSON(body)
var drive map[string]interface{}
err = json.Unmarshal([]byte(unquot), &drive)
if err != nil {
return "", err
}
if errnode, ok := drive["error"]; ok {
errmsg := errnode.(map[string]interface{})["message"].(string)
return "", errors.New(errmsg)
}
return drive["name"].(string), nil
}
func (s Graph) listChildren(path string) (map[string]interface{}, error) {
if len(s.groupId) == 0 {
return nil, errGroupIdMissing
}
token, err := s.getAuthoizationToken()
if err != nil {
return nil, err
}
// groupId := "612620b7-cd90-4b3f-a9bd-d34cddf52517"
var requrl string
if len(path) == 0 {
requrl = fmt.Sprintf("https://graph.microsoft.com/v1.0/groups/%s/drive/root/children", s.groupId)
} else {
requrl = fmt.Sprintf("https://graph.microsoft.com/v1.0/groups/%s/drive/root:/%s:/children", s.groupId, path)
}
req, _ := http.NewRequest("GET", requrl, nil)
req.Header.Add("Authorization", token)
req.Header.Add("Accept", "application/json")
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
defer func() {
io.Copy(io.Discard, resp.Body)
resp.Body.Close()
}()
body, _ := io.ReadAll(resp.Body)
unquot, _ := unescapeUnicodeCharactersInJSON(body)
var drive map[string]interface{}
err = json.Unmarshal([]byte(unquot), &drive)
if err != nil {
return nil, err
}
if errnode, ok := drive["error"]; ok {
errmsg := errnode.(map[string]interface{})["message"].(string)
return nil, errors.New(errmsg)
}
return drive, nil
}
func (s Graph) ListFolders(parent string) ([]string, error) {
drive, err := s.listChildren(parent)
if err != nil {
return nil, err
}
var output []string
items := drive["value"].([]interface{})
for _, item := range items {
itemobj := item.(map[string]interface{})
if _, ok := itemobj["file"]; !ok {
output = append(output, itemobj["name"].(string))
}
}
return output, nil
}
type FileMeta struct {
Path string `json:"path"`
LastModified time.Time `json:"lastModified"`
}
func (s Graph) ListFiles(parent string) ([]FileMeta, error) {
drive, err := s.listChildren(parent)
if err != nil {
return nil, err
}
var output []FileMeta
items := drive["value"].([]interface{})
for _, item := range items {
itemobj := item.(map[string]interface{})
if _, ok := itemobj["file"]; ok {
modTime, _ := time.Parse(time.RFC3339, itemobj["lastModifiedDateTime"].(string))
output = append(output, FileMeta{
Path: path.Join(parent, itemobj["name"].(string)),
LastModified: modTime,
})
}
}
return output, nil
}
func (s Graph) GetChannels() (map[string]string, error) {
if len(s.groupId) == 0 {
return nil, errGroupIdMissing
}
token, err := s.getAuthoizationToken()
if err != nil {
return nil, err
}
requrl := fmt.Sprintf("https://graph.microsoft.com/v1.0/teams/%s/channels", s.groupId)
req, _ := http.NewRequest("GET", requrl, nil)
req.Header.Add("Authorization", token)
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
defer func() {
io.Copy(io.Discard, resp.Body)
resp.Body.Close()
}()
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
var raw map[string]interface{}
if err = json.Unmarshal(body, &raw); err != nil {
return nil, err
}
if errnode, ok := raw["error"]; ok {
return nil, errors.New(errnode.(map[string]interface{})["message"].(string))
}
if r, ok := raw["value"]; ok {
valuenode := r.([]interface{})
output := make(map[string]string)
for _, v := range valuenode {
data := v.(map[string]interface{})
id := data["id"].(string)
displayname := data["displayName"].(string)
output[id] = displayname
}
return output, nil
}
return nil, errors.New("error")
}

261
azure/misc.go Normal file
View File

@ -0,0 +1,261 @@
package azure
import (
"crypto/md5"
"encoding/hex"
"errors"
"flag"
"fmt"
"io"
"os"
"os/exec"
"path"
"runtime"
"sort"
"strconv"
"strings"
"syscall"
"time"
"repositories.action2quare.com/ayo/gocommon/logger"
"go.mongodb.org/mongo-driver/bson"
)
func SortVersions(versions []string) []string {
sort.Slice(versions, func(i, j int) bool {
leftnum := 0
for _, iv := range strings.Split(versions[i], ".") {
n, _ := strconv.Atoi(iv)
leftnum += leftnum<<8 + n
}
rightnum := 0
for _, iv := range strings.Split(versions[j], ".") {
n, _ := strconv.Atoi(iv)
rightnum += rightnum<<8 + n
}
return leftnum < rightnum
})
return versions
}
const (
HoustonStatPoolCount = int(10)
)
type HoustonStatReport struct {
Region string `json:"region"`
FreeMemory float64 `json:"freeMemory"`
FreePercent float64 `json:"freePercent"`
CPUUsage float64 `json:"cpuUsage"`
PlayingUsers int `json:"users"`
PlayingSessions int `json:"sessions"`
}
var prefetchptr = flag.Bool("prefetch", false, "")
func NeedPrefetch() bool {
return *prefetchptr
}
var ErrUpdateUnnecessary = errors.New("binary is already latest")
func SelfUpdateUsingScript(replycache *MessageReplyCache, graph *Graph, patchroot string, force bool) (err error) {
// 1. 다운로드
var linkfilearg string
if replycache != nil {
if replycache.ReplyWrap == nil {
hostname, _ := os.Hostname()
replycache.ReplyWrap = replycache.SourceActivity().MakeWrap().WithPrefix(hostname)
}
linkfile := replycache.Serialize()
linkfilearg = fmt.Sprintf("-updatelink=%s", linkfile)
}
currentexe, _ := os.Executable()
currentexe = strings.ReplaceAll(currentexe, "\\", "/")
exefile := path.Base(currentexe)
if runtime.GOOS == "windows" && !strings.HasSuffix(exefile, ".exe") {
exefile += ".exe"
}
newbinary := fmt.Sprintf("%s.%s", exefile, time.Now().Format("2006.01.02-15.04.05"))
downloadurl := path.Join(patchroot, exefile)
err = graph.DownloadFile(downloadurl, newbinary)
if err != nil {
return
}
_, err = os.Stat(newbinary)
if os.IsNotExist(err) {
err = errors.New("다운로드 실패 : " + newbinary)
return
}
if !force {
currentfile, e := os.Open(currentexe)
if e != nil {
return e
}
defer currentfile.Close()
hash1 := md5.New()
_, err = io.Copy(hash1, currentfile)
if err != nil {
return
}
currentHash := hex.EncodeToString(hash1.Sum(nil))
nextfile, e := os.Open(newbinary)
if e != nil {
return e
}
defer nextfile.Close()
hash2 := md5.New()
_, err = io.Copy(hash2, nextfile)
if err != nil {
return
}
nextHash := hex.EncodeToString(hash2.Sum(nil))
if currentHash == nextHash {
// 해시가 같으니까 업데이트 할 필요가 없다.
return ErrUpdateUnnecessary
}
}
var scriptfileName string
if runtime.GOOS == "linux" {
scriptfileName = "selfupdate.sh"
} else if runtime.GOOS == "windows" {
scriptfileName = "selfupdate.bat"
}
scripturl := path.Join(patchroot, scriptfileName)
if _, err = os.Stat(scriptfileName); os.IsExist(err) {
err = os.Remove(scriptfileName)
} else {
err = nil
}
if err != nil {
return
}
// 2. 스크립트 다운로드
err = graph.DownloadFile(scripturl, scriptfileName)
if err != nil {
return
}
var nextArgs []string
for _, arg := range os.Args[1:] {
// -updatelink : selfupdate시에
if !strings.HasPrefix(arg, "-updatelink=") && arg != "-prefetch" {
nextArgs = append(nextArgs, arg)
}
}
pid := strconv.Itoa(os.Getpid())
args := append([]string{
pid,
newbinary,
exefile,
}, nextArgs...)
if len(linkfilearg) > 0 {
args = append(args, linkfilearg)
}
// 3. 독립 실행
if runtime.GOOS == "linux" {
// 실행 가능한 권한 부여
err = os.Chmod(scriptfileName, 0777)
if err != nil {
return
}
// 실행
env := os.Environ()
currentpath := path.Dir(currentexe)
argv0 := path.Join(currentpath, scriptfileName)
args = append([]string{"/bin/bash", argv0}, args...)
err = syscall.Exec(args[0], args, env)
if err != nil {
return
}
} else if runtime.GOOS == "windows" {
windowsargs := append([]string{
"/C",
"start",
scriptfileName,
}, args...)
cmd := exec.Command("cmd.exe", windowsargs...)
err = cmd.Run()
if err != nil {
return
}
}
return
}
var linkupdate = flag.String("updatelink", "", "")
func ReplyUpdateComplete() {
defer func() {
r := recover()
if r != nil {
logger.Error(r)
}
}()
if len(*linkupdate) > 0 {
cache := DeserializeMessageReplyCache(*linkupdate)
if cache != nil {
os.Remove(*linkupdate)
if cache.ReplyWrap != nil {
cache.ReplyWrap.Update(cache.Replyaid, "업데이트 완료")
}
}
}
}
// var objectIDCounter = readRandomUint32()
// var processUnique = processUniqueBytes()
// func processUniqueBytes() [5]byte {
// var b [5]byte
// _, err := io.ReadFull(rand.Reader, b[:])
// if err != nil {
// panic(fmt.Errorf("cannot initialize objectid package with crypto.rand.Reader: %v", err))
// }
// return b
// }
// func readRandomUint32() uint32 {
// var b [4]byte
// _, err := io.ReadFull(rand.Reader, b[:])
// if err != nil {
// panic(fmt.Errorf("cannot initialize objectid package with crypto.rand.Reader: %v", err))
// }
// return (uint32(b[0]) << 0) | (uint32(b[1]) << 8) | (uint32(b[2]) << 16) | (uint32(b[3]) << 24)
// }
type BsonMarshaler[T any] struct {
val T
}
func NewBsonMarshaler[T any](val T) *BsonMarshaler[T] {
return &BsonMarshaler[T]{
val: val,
}
}
func (m *BsonMarshaler[T]) MarshalBinary() (data []byte, err error) {
return bson.Marshal(m.val)
}

202
azure/monitor.go Normal file
View File

@ -0,0 +1,202 @@
package azure
import (
"bytes"
"encoding/json"
"fmt"
"io"
"math"
"net/http"
"net/url"
)
type CounterCurcuit struct {
curcuit []float64
cursor int
}
func (cc *CounterCurcuit) Append(val float64) {
cc.curcuit[cc.cursor] = val
cc.cursor++
}
func (cc *CounterCurcuit) Stat() (min float64, max float64, sum float64, count int) {
if cc.cursor == 0 {
return 0, 0, 0, 0
}
min = math.MaxFloat64
max = -math.MaxFloat64
sum = float64(0)
if cc.cursor < len(cc.curcuit) {
count = cc.cursor
for i := 0; i < cc.cursor; i++ {
if min > cc.curcuit[i] {
min = cc.curcuit[i]
}
if max < cc.curcuit[i] {
max = cc.curcuit[i]
}
sum += cc.curcuit[i]
}
return
}
count = len(cc.curcuit)
for _, v := range cc.curcuit {
if min > v {
min = v
}
if max < v {
max = v
}
sum += v
}
return
}
func MakeCounterCurcuit(length int) *CounterCurcuit {
return &CounterCurcuit{
curcuit: make([]float64, length),
cursor: 0,
}
}
type Float64Counter struct {
Min float64 `json:"min"`
Max float64 `json:"max"`
Sum float64 `json:"sum"`
Count int `json:"count"`
}
var DefaultFloat64Counter = Float64Counter{
Min: math.MaxFloat64,
Max: -math.MaxFloat64,
Sum: 0,
Count: 0,
}
func (c *Float64Counter) Valid() bool {
return c.Count > 0
}
func (c *Float64Counter) Add(val float64) {
if c.Max < val {
c.Max = val
}
if c.Min > val {
c.Min = val
}
c.Sum += val
c.Count++
}
func (c *Float64Counter) SingleMin() Float64Counter {
return Float64Counter{
Min: c.Min,
Max: c.Min,
Sum: c.Min,
Count: 1,
}
}
func (c *Float64Counter) SingleMax() Float64Counter {
return Float64Counter{
Min: c.Max,
Max: c.Max,
Sum: c.Max,
Count: 1,
}
}
func (c *Float64Counter) AddInt(val int) {
fval := float64(val)
if c.Max < fval {
c.Max = fval
}
if c.Min > fval {
c.Min = fval
}
c.Sum += fval
c.Count++
}
type MetricSeries struct {
Float64Counter
DimValues []string `json:"dimValues"`
}
type MetructBaseData struct {
Metric string `json:"metric"`
Namespace string `json:"namespace"`
DimNames []string `json:"dimNames"`
Series []MetricSeries `json:"series"`
}
type MetricData struct {
BaseData MetructBaseData `json:"baseData"`
}
type MetricDocument struct {
Time string `json:"time"`
Data MetricData `json:"data"`
}
func MakeMetricDocument(timestamp string, namespace string, metric string, dimNames []string) MetricDocument {
output := MetricDocument{Time: timestamp}
output.Data.BaseData.DimNames = dimNames
output.Data.BaseData.Metric = metric
output.Data.BaseData.Namespace = namespace
return output
}
type Monitor struct {
*accessToken
tenantId string
}
func NewMonitor(tenantId string) *Monitor {
return &Monitor{
accessToken: &accessToken{
url: fmt.Sprintf("https://login.microsoftonline.com/%s/oauth2/token", tenantId),
values: url.Values{
"grant_type": {"client_credentials"},
"client_id": {microsoftAppId()},
"resource": {"https://monitoring.azure.com/"},
"client_secret": {microsoftAppPassword()},
},
},
tenantId: tenantId,
}
}
func (m Monitor) PostMetrics(url string, docs ...MetricDocument) error {
// https://docs.microsoft.com/ko-kr/rest/api/monitor/metrics%20(data%20plane)/create
token, err := m.getAuthoizationToken()
if err != nil {
return err
}
for _, doc := range docs {
bt, _ := json.Marshal(doc)
req, _ := http.NewRequest("POST", url, bytes.NewReader(bt))
req.Header.Add("Authorization", token)
req.Header.Add("Content-Type", "application/json")
resp, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
io.Copy(io.Discard, resp.Body)
resp.Body.Close()
}
return nil
}