@@ -15,13 +15,9 @@ type CustomMigrationStatic struct { | |||
Migrate func(*xorm.Engine, *xorm.Engine) error | |||
} | |||
var customMigrations = []CustomMigration{ | |||
{"Custom v1 Topic struct change to support chinese", syncTopicStruct}, | |||
} | |||
var customMigrations []CustomMigration | |||
var customMigrationsStatic = []CustomMigrationStatic{ | |||
{"update issue_fixed_rate to 1 if num_issues is 0 ", updateIssueFixedRate}, | |||
} | |||
var customMigrationsStatic []CustomMigrationStatic | |||
func MigrateCustom(x *xorm.Engine) { | |||
@@ -181,6 +181,7 @@ func SearchDatasetCondition(opts *SearchDatasetOptions) builder.Cond { | |||
if len(opts.DatasetIDs) > 0 { | |||
subCon := builder.NewCond() | |||
subCon = subCon.And(builder.In("dataset.id", opts.DatasetIDs)) | |||
subCon = generateFilterCond(opts, subCon) | |||
cond = cond.Or(subCon) | |||
} | |||
@@ -460,5 +461,12 @@ func GetCollaboratorDatasetIdsByUserID(userID int64) []int64 { | |||
_ = x.Table("dataset").Join("INNER", "collaboration", "dataset.repo_id = collaboration.repo_id and collaboration.mode>0 and collaboration.user_id=?", userID). | |||
Cols("dataset.id").Find(&datasets) | |||
return datasets | |||
} | |||
func GetTeamDatasetIdsByUserID(userID int64) []int64 { | |||
var datasets []int64 | |||
_ = x.Table("dataset").Join("INNER", "team_repo", "dataset.repo_id = team_repo.repo_id"). | |||
Join("INNER", "team_user", "team_repo.team_id=team_user.team_id and team_user.uid=?", userID). | |||
Cols("dataset.id").Find(&datasets) | |||
return datasets | |||
} |
@@ -2749,15 +2749,10 @@ func ReadLatestFileInRepo(userName, repoName, refName, treePath string) (*RepoFi | |||
log.Error("ReadLatestFileInRepo: Close: %v", err) | |||
} | |||
}() | |||
buf := make([]byte, 1024) | |||
n, _ := reader.Read(buf) | |||
if n >= 0 { | |||
buf = buf[:n] | |||
} | |||
d, _ := ioutil.ReadAll(reader) | |||
commitId := "" | |||
if blob != nil { | |||
commitId = fmt.Sprint(blob.ID) | |||
} | |||
return &RepoFile{CommitId: commitId, Content: buf}, nil | |||
return &RepoFile{CommitId: commitId, Content: d}, nil | |||
} |
@@ -0,0 +1,139 @@ | |||
package wechat | |||
import ( | |||
"code.gitea.io/gitea/models" | |||
"code.gitea.io/gitea/modules/log" | |||
"code.gitea.io/gitea/modules/setting" | |||
"encoding/json" | |||
"github.com/patrickmn/go-cache" | |||
"strings" | |||
"time" | |||
) | |||
var WechatReplyCache = cache.New(2*time.Minute, 1*time.Minute) | |||
const ( | |||
WECHAT_REPLY_CACHE_KEY = "wechat_response" | |||
) | |||
const ( | |||
ReplyTypeText = "text" | |||
ReplyTypeImage = "image" | |||
ReplyTypeVoice = "voice" | |||
ReplyTypeVideo = "video" | |||
ReplyTypeMusic = "music" | |||
ReplyTypeNews = "news" | |||
) | |||
type ReplyConfigType string | |||
const ( | |||
SubscribeReply ReplyConfigType = "subscribe" | |||
AutoMsgReply ReplyConfigType = "autoMsg" | |||
) | |||
func (r ReplyConfigType) Name() string { | |||
switch r { | |||
case SubscribeReply: | |||
return "subscribe" | |||
case AutoMsgReply: | |||
return "autoMsg" | |||
} | |||
return "" | |||
} | |||
func (r ReplyConfigType) TreePath() string { | |||
switch r { | |||
case SubscribeReply: | |||
return setting.TreePathOfSubscribe | |||
case AutoMsgReply: | |||
return setting.TreePathOfAutoMsgReply | |||
} | |||
return "" | |||
} | |||
type WechatReplyContent struct { | |||
Reply *ReplyContent | |||
ReplyType string | |||
KeyWords []string | |||
IsFullMatch int | |||
} | |||
type ReplyContent struct { | |||
Content string | |||
MediaId string | |||
Title string | |||
Description string | |||
MusicUrl string | |||
HQMusicUrl string | |||
ThumbMediaId string | |||
Articles []ArticlesContent | |||
} | |||
func GetAutomaticReply(msg string) *WechatReplyContent { | |||
r, err := LoadReplyFromCacheAndDisk(AutoMsgReply) | |||
if err != nil { | |||
return nil | |||
} | |||
if r == nil || len(r) == 0 { | |||
return nil | |||
} | |||
for i := 0; i < len(r); i++ { | |||
if r[i].IsFullMatch == 0 { | |||
for _, v := range r[i].KeyWords { | |||
if strings.Contains(msg, v) { | |||
return r[i] | |||
} | |||
} | |||
} else if r[i].IsFullMatch > 0 { | |||
for _, v := range r[i].KeyWords { | |||
if msg == v { | |||
return r[i] | |||
} | |||
} | |||
} | |||
} | |||
return nil | |||
} | |||
func loadReplyFromDisk(replyConfig ReplyConfigType) ([]*WechatReplyContent, error) { | |||
log.Info("LoadReply from disk") | |||
repo, err := models.GetRepositoryByOwnerAndAlias(setting.UserNameOfWechatReply, setting.RepoNameOfWechatReply) | |||
if err != nil { | |||
log.Error("get AutomaticReply repo failed, error=%v", err) | |||
return nil, err | |||
} | |||
repoFile, err := models.ReadLatestFileInRepo(setting.UserNameOfWechatReply, repo.Name, setting.RefNameOfWechatReply, replyConfig.TreePath()) | |||
if err != nil { | |||
log.Error("get AutomaticReply failed, error=%v", err) | |||
return nil, err | |||
} | |||
res := make([]*WechatReplyContent, 0) | |||
json.Unmarshal(repoFile.Content, &res) | |||
if res == nil || len(res) == 0 { | |||
return nil, err | |||
} | |||
return res, nil | |||
} | |||
func LoadReplyFromCacheAndDisk(replyConfig ReplyConfigType) ([]*WechatReplyContent, error) { | |||
v, success := WechatReplyCache.Get(replyConfig.Name()) | |||
if success { | |||
log.Info("LoadReply from cache,value = %v", v) | |||
if v == nil { | |||
return nil, nil | |||
} | |||
n := v.([]*WechatReplyContent) | |||
return n, nil | |||
} | |||
content, err := loadReplyFromDisk(replyConfig) | |||
if err != nil { | |||
log.Error("LoadReply failed, error=%v", err) | |||
WechatReplyCache.Set(replyConfig.Name(), nil, 30*time.Second) | |||
return nil, err | |||
} | |||
WechatReplyCache.Set(replyConfig.Name(), content, 60*time.Second) | |||
return content, nil | |||
} |
@@ -17,7 +17,8 @@ var ( | |||
const ( | |||
GRANT_TYPE = "client_credential" | |||
ACCESS_TOKEN_PATH = "/cgi-bin/token" | |||
QR_CODE_Path = "/cgi-bin/qrcode/create" | |||
QR_CODE_PATH = "/cgi-bin/qrcode/create" | |||
GET_MATERIAL_PATH = "/cgi-bin/material/batchget_material" | |||
ACTION_QR_STR_SCENE = "QR_STR_SCENE" | |||
ERR_CODE_ACCESSTOKEN_EXPIRE = 42001 | |||
@@ -40,6 +41,11 @@ type QRCodeRequest struct { | |||
Action_info ActionInfo `json:"action_info"` | |||
Expire_seconds int `json:"expire_seconds"` | |||
} | |||
type MaterialRequest struct { | |||
Type string `json:"type"` | |||
Offset int `json:"offset"` | |||
Count int `json:"count"` | |||
} | |||
type ActionInfo struct { | |||
Scene Scene `json:"scene"` | |||
@@ -97,7 +103,7 @@ func callQRCodeCreate(sceneStr string) (*QRCodeResponse, bool) { | |||
SetQueryParam("access_token", GetWechatAccessToken()). | |||
SetBody(bodyJson). | |||
SetResult(&result). | |||
Post(setting.WechatApiHost + QR_CODE_Path) | |||
Post(setting.WechatApiHost + QR_CODE_PATH) | |||
if err != nil { | |||
log.Error("create QR code failed,e=%v", err) | |||
return nil, false | |||
@@ -113,6 +119,37 @@ func callQRCodeCreate(sceneStr string) (*QRCodeResponse, bool) { | |||
return &result, false | |||
} | |||
//getMaterial | |||
// api doc: https://developers.weixin.qq.com/doc/offiaccount/Asset_Management/Get_materials_list.html | |||
func getMaterial(mType string, offset, count int) (interface{}, bool) { | |||
client := getWechatRestyClient() | |||
body := &MaterialRequest{ | |||
Type: mType, | |||
Offset: offset, | |||
Count: count, | |||
} | |||
bodyJson, _ := json.Marshal(body) | |||
r, err := client.R(). | |||
SetHeader("Content-Type", "application/json"). | |||
SetQueryParam("access_token", GetWechatAccessToken()). | |||
SetBody(bodyJson). | |||
Post(setting.WechatApiHost + GET_MATERIAL_PATH) | |||
if err != nil { | |||
log.Error("create QR code failed,e=%v", err) | |||
return nil, false | |||
} | |||
a := r.Body() | |||
resultMap := make(map[string]interface{}, 0) | |||
json.Unmarshal(a, &resultMap) | |||
errcode := resultMap["errcode"] | |||
if errcode == fmt.Sprint(ERR_CODE_ACCESSTOKEN_EXPIRE) || errcode == fmt.Sprint(ERR_CODE_ACCESSTOKEN_INVALID) { | |||
return nil, true | |||
} | |||
log.Info("%v", r) | |||
return &resultMap, false | |||
} | |||
func getErrorCodeFromResponse(r *resty.Response) int { | |||
a := r.Body() | |||
resultMap := make(map[string]interface{}, 0) | |||
@@ -18,7 +18,7 @@ import ( | |||
// <EventKey><![CDATA[SCENE_VALUE]]></EventKey> | |||
// <Ticket><![CDATA[TICKET]]></Ticket> | |||
//</xml> | |||
type WechatEvent struct { | |||
type WechatMsg struct { | |||
ToUserName string | |||
FromUserName string | |||
CreateTime int64 | |||
@@ -26,9 +26,13 @@ type WechatEvent struct { | |||
Event string | |||
EventKey string | |||
Ticket string | |||
Content string | |||
MsgId string | |||
MsgDataId string | |||
Idx string | |||
} | |||
type EventReply struct { | |||
type MsgReply struct { | |||
XMLName xml.Name `xml:"xml"` | |||
ToUserName string | |||
FromUserName string | |||
@@ -37,16 +41,97 @@ type EventReply struct { | |||
Content string | |||
} | |||
type TextMsgReply struct { | |||
XMLName xml.Name `xml:"xml"` | |||
ToUserName string | |||
FromUserName string | |||
CreateTime int64 | |||
MsgType string | |||
Content string | |||
} | |||
type ImageMsgReply struct { | |||
XMLName xml.Name `xml:"xml"` | |||
ToUserName string | |||
FromUserName string | |||
CreateTime int64 | |||
MsgType string | |||
Image ImageContent | |||
} | |||
type VoiceMsgReply struct { | |||
XMLName xml.Name `xml:"xml"` | |||
ToUserName string | |||
FromUserName string | |||
CreateTime int64 | |||
MsgType string | |||
Voice VoiceContent | |||
} | |||
type VideoMsgReply struct { | |||
XMLName xml.Name `xml:"xml"` | |||
ToUserName string | |||
FromUserName string | |||
CreateTime int64 | |||
MsgType string | |||
Video VideoContent | |||
} | |||
type MusicMsgReply struct { | |||
XMLName xml.Name `xml:"xml"` | |||
ToUserName string | |||
FromUserName string | |||
CreateTime int64 | |||
MsgType string | |||
Music MusicContent | |||
} | |||
type NewsMsgReply struct { | |||
XMLName xml.Name `xml:"xml"` | |||
ToUserName string | |||
FromUserName string | |||
CreateTime int64 | |||
MsgType string | |||
ArticleCount int | |||
Articles ArticleItem | |||
} | |||
type ArticleItem struct { | |||
Item []ArticlesContent | |||
} | |||
type ImageContent struct { | |||
MediaId string | |||
} | |||
type VoiceContent struct { | |||
MediaId string | |||
} | |||
type VideoContent struct { | |||
MediaId string | |||
Title string | |||
Description string | |||
} | |||
type MusicContent struct { | |||
Title string | |||
Description string | |||
MusicUrl string | |||
HQMusicUrl string | |||
ThumbMediaId string | |||
} | |||
type ArticlesContent struct { | |||
XMLName xml.Name `xml:"item"` | |||
Title string | |||
Description string | |||
PicUrl string | |||
Url string | |||
} | |||
const ( | |||
WECHAT_EVENT_SUBSCRIBE = "subscribe" | |||
WECHAT_EVENT_SCAN = "SCAN" | |||
) | |||
const ( | |||
WECHAT_MSG_TYPE_TEXT = "text" | |||
WECHAT_MSG_TYPE_TEXT = "text" | |||
WECHAT_MSG_TYPE_EVENT = "event" | |||
) | |||
func HandleSubscribeEvent(we WechatEvent) string { | |||
func HandleScanEvent(we WechatMsg) string { | |||
eventKey := we.EventKey | |||
if eventKey == "" { | |||
return "" | |||
@@ -74,3 +159,11 @@ func HandleSubscribeEvent(we WechatEvent) string { | |||
return BIND_REPLY_SUCCESS | |||
} | |||
func HandleSubscribeEvent(we WechatMsg) *WechatReplyContent { | |||
r, err := LoadReplyFromCacheAndDisk(SubscribeReply) | |||
if err != nil || len(r) == 0 { | |||
return nil | |||
} | |||
return r[0] | |||
} |
@@ -0,0 +1,13 @@ | |||
package wechat | |||
import "code.gitea.io/gitea/modules/log" | |||
func GetWechatMaterial(mType string, offset, count int) interface{} { | |||
result, retryFlag := getMaterial(mType, offset, count) | |||
if retryFlag { | |||
log.Info("retryGetWechatMaterial calling") | |||
refreshAccessToken() | |||
result, _ = getMaterial(mType, offset, count) | |||
} | |||
return result | |||
} |
@@ -553,6 +553,13 @@ var ( | |||
WechatQRCodeExpireSeconds int | |||
WechatAuthSwitch bool | |||
//wechat auto reply config | |||
UserNameOfWechatReply string | |||
RepoNameOfWechatReply string | |||
RefNameOfWechatReply string | |||
TreePathOfAutoMsgReply string | |||
TreePathOfSubscribe string | |||
//nginx proxy | |||
PROXYURL string | |||
RadarMap = struct { | |||
@@ -1380,6 +1387,11 @@ func NewContext() { | |||
WechatAppSecret = sec.Key("APP_SECRET").MustString("e48e13f315adc32749ddc7057585f198") | |||
WechatQRCodeExpireSeconds = sec.Key("QR_CODE_EXPIRE_SECONDS").MustInt(120) | |||
WechatAuthSwitch = sec.Key("AUTH_SWITCH").MustBool(true) | |||
UserNameOfWechatReply = sec.Key("AUTO_REPLY_USER_NAME").MustString("OpenIOSSG") | |||
RepoNameOfWechatReply = sec.Key("AUTO_REPLY_REPO_NAME").MustString("promote") | |||
RefNameOfWechatReply = sec.Key("AUTO_REPLY_REF_NAME").MustString("master") | |||
TreePathOfAutoMsgReply = sec.Key("AUTO_REPLY_TREE_PATH").MustString("wechat/auto_reply.json") | |||
TreePathOfSubscribe = sec.Key("SUBSCRIBE_TREE_PATH").MustString("wechat/subscribe_reply.json") | |||
SetRadarMapConfig() | |||
@@ -1423,7 +1423,7 @@ issues.label_templates.helper=选择标签模板 | |||
issues.label_templates.use=使用标签集 | |||
issues.label_templates.fail_to_load_file=加载标签模板文件 '%s' 时发生错误:%v | |||
issues.add_label_at=添加了标签 <div class="ui label" style="color: %s\; background-color: %s"> %s </div> %s | |||
issues.remove_label_at=删除了 <div class="ui label" style="color: %s\; background-color: %s">%s</div> label %s 标签 | |||
issues.remove_label_at=删除了标签 <div class="ui label" style="color: %s\; background-color: %s">%s </div> %s | |||
issues.add_milestone_at=` %[2]s 添加了里程碑 <b>%[1]s</b>` | |||
issues.change_milestone_at=`%[3]s 修改了里程碑从 <b>%[1]s</b> 到 <b>%[2]s</b>` | |||
issues.remove_milestone_at=`%[2]s 删除了里程碑 <b>%[1]s</b>` | |||
@@ -74,28 +74,30 @@ var swiperOrg = new Swiper(".homeorg-list", { | |||
}, | |||
}); | |||
var output = document.getElementById("newmessage"); | |||
var url = "ws://" + document.location.host + "/action/notification"; | |||
if(document.location.host == "git.openi.org.cn" || document.URL.startsWith("https")){ | |||
url = "wss://" + document.location.host + "/action/notification" | |||
} | |||
var socket = new WebSocket(url); | |||
socket.onopen = function () { | |||
messageQueue = []; | |||
console.log("message has connected."); | |||
}; | |||
var maxSize = 20; | |||
var html =document.documentElement; | |||
var lang = html.attributes["lang"] | |||
var isZh = true; | |||
if(lang != null && lang.nodeValue =="en-US" ){ | |||
isZh=false; | |||
}else{ | |||
} | |||
socket.onmessage = function (e) { | |||
document.onreadystatechange = function () { | |||
queryRecommendData(); | |||
var output = document.getElementById("newmessage"); | |||
var url = "ws://" + document.location.host + "/action/notification"; | |||
if(document.location.host == "git.openi.org.cn" || document.URL.startsWith("https")){ | |||
url = "wss://" + document.location.host + "/action/notification" | |||
} | |||
var socket = new WebSocket(url); | |||
socket.onopen = function () { | |||
messageQueue = []; | |||
console.log("message has connected."); | |||
}; | |||
socket.onmessage = function (e) { | |||
var data =JSON.parse(e.data) | |||
var html = ""; | |||
if (data != null){ | |||
@@ -176,7 +178,10 @@ socket.onmessage = function (e) { | |||
output.innerHTML = html; | |||
swiperNewMessage.updateSlides(); | |||
swiperNewMessage.updateProgress(); | |||
}; | |||
}; | |||
} | |||
function getTaskLink(record){ | |||
var re = getRepoLink(record); | |||
@@ -437,7 +442,9 @@ function getAction(opType,isZh){ | |||
} | |||
} | |||
queryRecommendData(); | |||
function queryRecommendData(){ | |||
$.ajax({ | |||
@@ -1055,6 +1055,7 @@ func RegisterRoutes(m *macaron.Macaron) { | |||
m.Get("/prd/event", authentication.ValidEventSource) | |||
m.Post("/prd/event", authentication.AcceptWechatEvent) | |||
}) | |||
m.Get("/wechat/material", authentication.GetMaterial) | |||
}, securityHeaders(), context.APIContexter(), sudo()) | |||
} | |||
@@ -8,9 +8,11 @@ import ( | |||
"code.gitea.io/gitea/modules/redis/redis_client" | |||
"code.gitea.io/gitea/modules/redis/redis_key" | |||
"code.gitea.io/gitea/modules/setting" | |||
"code.gitea.io/gitea/routers/response" | |||
"encoding/json" | |||
"errors" | |||
gouuid "github.com/satori/go.uuid" | |||
"strconv" | |||
"time" | |||
) | |||
@@ -124,3 +126,23 @@ func createQRCode4Bind(userId int64) (*QRCodeResponse, error) { | |||
} | |||
return result, nil | |||
} | |||
// GetMaterial | |||
func GetMaterial(ctx *context.Context) { | |||
mType := ctx.Query("type") | |||
offsetStr := ctx.Query("offset") | |||
countStr := ctx.Query("count") | |||
var offset, count int | |||
if offsetStr == "" { | |||
offset = 0 | |||
} else { | |||
offset, _ = strconv.Atoi(offsetStr) | |||
} | |||
if countStr == "" { | |||
count = 20 | |||
} else { | |||
count, _ = strconv.Atoi(countStr) | |||
} | |||
r := wechat.GetWechatMaterial(mType, offset, count) | |||
ctx.JSON(200, response.SuccessWithData(r)) | |||
} |
@@ -14,24 +14,48 @@ import ( | |||
// https://developers.weixin.qq.com/doc/offiaccount/Message_Management/Passive_user_reply_message.html | |||
func AcceptWechatEvent(ctx *context.Context) { | |||
b, _ := ioutil.ReadAll(ctx.Req.Request.Body) | |||
we := wechat.WechatEvent{} | |||
we := wechat.WechatMsg{} | |||
xml.Unmarshal(b, &we) | |||
switch we.MsgType { | |||
case wechat.WECHAT_MSG_TYPE_EVENT: | |||
HandleEventMsg(ctx, we) | |||
case wechat.WECHAT_MSG_TYPE_TEXT: | |||
HandleTextMsg(ctx, we) | |||
} | |||
log.Info("accept wechat event= %+v", we) | |||
var replyStr string | |||
switch we.Event { | |||
case wechat.WECHAT_EVENT_SUBSCRIBE, wechat.WECHAT_EVENT_SCAN: | |||
replyStr = wechat.HandleSubscribeEvent(we) | |||
break | |||
} | |||
// ValidEventSource | |||
func ValidEventSource(ctx *context.Context) { | |||
echostr := ctx.Query("echostr") | |||
ctx.Write([]byte(echostr)) | |||
return | |||
} | |||
func HandleEventMsg(ctx *context.Context, msg wechat.WechatMsg) { | |||
switch msg.Event { | |||
case wechat.WECHAT_EVENT_SCAN: | |||
HandleEventScan(ctx, msg) | |||
case wechat.WECHAT_EVENT_SUBSCRIBE: | |||
if msg.EventKey != "" { | |||
HandleEventScan(ctx, msg) | |||
} else { | |||
HandleEventSubscribe(ctx, msg) | |||
} | |||
} | |||
} | |||
func HandleEventScan(ctx *context.Context, msg wechat.WechatMsg) { | |||
replyStr := wechat.HandleScanEvent(msg) | |||
if replyStr == "" { | |||
log.Info("reply str is empty") | |||
return | |||
} | |||
reply := &wechat.EventReply{ | |||
ToUserName: we.FromUserName, | |||
FromUserName: we.ToUserName, | |||
reply := &wechat.MsgReply{ | |||
ToUserName: msg.FromUserName, | |||
FromUserName: msg.ToUserName, | |||
CreateTime: time.Now().Unix(), | |||
MsgType: wechat.WECHAT_MSG_TYPE_TEXT, | |||
Content: replyStr, | |||
@@ -39,9 +63,99 @@ func AcceptWechatEvent(ctx *context.Context) { | |||
ctx.XML(200, reply) | |||
} | |||
// ValidEventSource | |||
func ValidEventSource(ctx *context.Context) { | |||
echostr := ctx.Query("echostr") | |||
ctx.Write([]byte(echostr)) | |||
return | |||
func HandleEventSubscribe(ctx *context.Context, msg wechat.WechatMsg) { | |||
r := wechat.HandleSubscribeEvent(msg) | |||
if r == nil { | |||
return | |||
} | |||
reply := buildReplyContent(msg, r) | |||
ctx.XML(200, reply) | |||
} | |||
func HandleTextMsg(ctx *context.Context, msg wechat.WechatMsg) { | |||
r := wechat.GetAutomaticReply(msg.Content) | |||
if r == nil { | |||
log.Info("TextMsg reply is empty") | |||
return | |||
} | |||
reply := buildReplyContent(msg, r) | |||
ctx.XML(200, reply) | |||
} | |||
func buildReplyContent(msg wechat.WechatMsg, r *wechat.WechatReplyContent) interface{} { | |||
reply := &wechat.MsgReply{ | |||
ToUserName: msg.FromUserName, | |||
FromUserName: msg.ToUserName, | |||
CreateTime: time.Now().Unix(), | |||
MsgType: r.ReplyType, | |||
} | |||
switch r.ReplyType { | |||
case wechat.ReplyTypeText: | |||
return &wechat.TextMsgReply{ | |||
ToUserName: msg.FromUserName, | |||
FromUserName: msg.ToUserName, | |||
CreateTime: time.Now().Unix(), | |||
MsgType: r.ReplyType, | |||
Content: r.Reply.Content, | |||
} | |||
case wechat.ReplyTypeImage: | |||
return &wechat.ImageMsgReply{ | |||
ToUserName: msg.FromUserName, | |||
FromUserName: msg.ToUserName, | |||
CreateTime: time.Now().Unix(), | |||
MsgType: r.ReplyType, | |||
Image: wechat.ImageContent{ | |||
MediaId: r.Reply.MediaId, | |||
}, | |||
} | |||
case wechat.ReplyTypeVoice: | |||
return &wechat.VoiceMsgReply{ | |||
ToUserName: msg.FromUserName, | |||
FromUserName: msg.ToUserName, | |||
CreateTime: time.Now().Unix(), | |||
MsgType: r.ReplyType, | |||
Voice: wechat.VoiceContent{ | |||
MediaId: r.Reply.MediaId, | |||
}, | |||
} | |||
case wechat.ReplyTypeVideo: | |||
return &wechat.VideoMsgReply{ | |||
ToUserName: msg.FromUserName, | |||
FromUserName: msg.ToUserName, | |||
CreateTime: time.Now().Unix(), | |||
MsgType: r.ReplyType, | |||
Video: wechat.VideoContent{ | |||
MediaId: r.Reply.MediaId, | |||
Title: r.Reply.Title, | |||
Description: r.Reply.Description, | |||
}, | |||
} | |||
case wechat.ReplyTypeMusic: | |||
return &wechat.MusicMsgReply{ | |||
ToUserName: msg.FromUserName, | |||
FromUserName: msg.ToUserName, | |||
CreateTime: time.Now().Unix(), | |||
MsgType: r.ReplyType, | |||
Music: wechat.MusicContent{ | |||
Title: r.Reply.Title, | |||
Description: r.Reply.Description, | |||
MusicUrl: r.Reply.MusicUrl, | |||
HQMusicUrl: r.Reply.HQMusicUrl, | |||
ThumbMediaId: r.Reply.ThumbMediaId, | |||
}, | |||
} | |||
case wechat.ReplyTypeNews: | |||
return &wechat.NewsMsgReply{ | |||
ToUserName: msg.FromUserName, | |||
FromUserName: msg.ToUserName, | |||
CreateTime: time.Now().Unix(), | |||
MsgType: r.ReplyType, | |||
ArticleCount: len(r.Reply.Articles), | |||
Articles: wechat.ArticleItem{ | |||
Item: r.Reply.Articles}, | |||
} | |||
} | |||
return reply | |||
} |
@@ -345,7 +345,9 @@ func ExploreDatasets(ctx *context.Context) { | |||
var datasetsIds []int64 | |||
if ownerID > 0 { | |||
datasetsIds = models.GetCollaboratorDatasetIdsByUserID(ownerID) | |||
collaboratorDatasetsIds := models.GetCollaboratorDatasetIdsByUserID(ownerID) | |||
teamDatasetsIds := models.GetTeamDatasetIdsByUserID(ownerID) | |||
datasetsIds = append(collaboratorDatasetsIds, teamDatasetsIds...) | |||
} | |||
opts := &models.SearchDatasetOptions{ | |||
@@ -172,8 +172,8 @@ func DatasetIndex(ctx *context.Context) { | |||
for _, attachment := range pageAttachments { | |||
uploader, _ := models.GetUserByID(attachment.UploaderID) | |||
attachment.Uploader = uploader | |||
if !strings.HasSuffix(attachment.Name, ".zip") { | |||
attachment.DecompressState = -1 //非zip文件 | |||
if !strings.HasSuffix(attachment.Name, ".zip") && !strings.HasSuffix(attachment.Name, ".tar.gz") { | |||
attachment.DecompressState = -1 //非压缩文件 | |||
} | |||
} | |||
@@ -616,8 +616,14 @@ func DatasetIsCollaborator(ctx *context.Context, dataset *models.Dataset) bool { | |||
repo.GetOwner() | |||
if ctx.User != nil { | |||
if repo.Owner.IsOrganization() { | |||
if repo.Owner.IsUserPartOfOrg(ctx.User.ID) { | |||
for _, t := range repo.Owner.Teams { | |||
org := repo.Owner | |||
org.Teams, err = org.GetUserTeams(ctx.User.ID) | |||
if err != nil { | |||
log.Error("GetUserTeams error:", err.Error()) | |||
return false | |||
} | |||
if org.IsUserPartOfOrg(ctx.User.ID) { | |||
for _, t := range org.Teams { | |||
if t.IsMember(ctx.User.ID) && t.HasRepository(repo.ID) { | |||
return true | |||
} | |||
@@ -18,7 +18,7 @@ | |||
.ui.list .list>.item>img.image+.content, | |||
.ui.list>.item>img.image+.content { | |||
width: calc(100% - 30px); | |||
width: calc(100% - 34px); | |||
margin-left: 0; | |||
} | |||
@@ -107,9 +107,9 @@ | |||
{{range .Repos}} | |||
<div class="item"> | |||
{{if .RelAvatarLink}} | |||
<img class="ui avatar image" src="{{.RelAvatarLink}}"> | |||
<img class="ui avatar image" style="width: 28px;height: 28px;" src="{{.RelAvatarLink}}"> | |||
{{else}} | |||
<img class="ui avatar image" avatar="{{.Owner.Name}}"> | |||
<img class="ui avatar image" style="width: 28px;height: 28px;" avatar="{{.Owner.Name}}"> | |||
{{end}} | |||
<div class="content"> | |||
<div class="ui header"> | |||
@@ -1,74 +1,100 @@ | |||
/** | |||
* LetterAvatar | |||
* | |||
* | |||
* Artur Heinze | |||
* Create Letter avatar based on Initials | |||
* based on https://gist.github.com/leecrossley/6027780 | |||
*/ | |||
(function(w, d){ | |||
function LetterAvatar (name, size, color) { | |||
name = name || ''; | |||
size = size || 60; | |||
var colours = [ | |||
"#1abc9c", "#2ecc71", "#3498db", "#9b59b6", "#34495e", "#16a085", "#27ae60", "#2980b9", "#8e44ad", "#2c3e50", | |||
"#f1c40f", "#e67e22", "#e74c3c", "#00bcd4", "#95a5a6", "#f39c12", "#d35400", "#c0392b", "#bdc3c7", "#7f8c8d" | |||
], | |||
nameSplit = String(name).split(' '), | |||
initials, charIndex, colourIndex, canvas, context, dataURI; | |||
if (nameSplit.length == 1) { | |||
initials = nameSplit[0] ? nameSplit[0].charAt(0):'?'; | |||
} else { | |||
initials = nameSplit[0].charAt(0) + nameSplit[1].charAt(0); | |||
} | |||
if (w.devicePixelRatio) { | |||
size = (size * w.devicePixelRatio); | |||
} | |||
charIndex = (initials == '?' ? 72 : initials.charCodeAt(0)) - 64; | |||
colourIndex = charIndex % 20; | |||
canvas = d.createElement('canvas'); | |||
canvas.width = size; | |||
canvas.height = size; | |||
context = canvas.getContext("2d"); | |||
context.fillStyle = color ? color : colours[colourIndex - 1]; | |||
context.fillRect (0, 0, canvas.width, canvas.height); | |||
context.font = Math.round(canvas.width/2)+"px 'Microsoft Yahei'"; | |||
context.textAlign = "center"; | |||
context.fillStyle = "#FFF"; | |||
context.fillText(initials, size / 2, size / 1.5); | |||
dataURI = canvas.toDataURL(); | |||
canvas = null; | |||
return dataURI; | |||
(function (w, d) { | |||
function LetterAvatar(name, size, color) { | |||
name = name || ""; | |||
size = size || 60; | |||
var colours = [ | |||
"#1abc9c", | |||
"#2ecc71", | |||
"#3498db", | |||
"#9b59b6", | |||
"#34495e", | |||
"#16a085", | |||
"#27ae60", | |||
"#2980b9", | |||
"#8e44ad", | |||
"#2c3e50", | |||
"#f1c40f", | |||
"#e67e22", | |||
"#e74c3c", | |||
"#00bcd4", | |||
"#95a5a6", | |||
"#f39c12", | |||
"#d35400", | |||
"#c0392b", | |||
"#bdc3c7", | |||
"#7f8c8d", | |||
], | |||
nameSplit = String(name).split(" "), | |||
initials, | |||
charIndex, | |||
colourIndex, | |||
canvas, | |||
context, | |||
dataURI; | |||
if (nameSplit.length == 1) { | |||
initials = nameSplit[0] ? nameSplit[0].charAt(0) : "?"; | |||
} else { | |||
initials = nameSplit[0].charAt(0) + nameSplit[1].charAt(0); | |||
} | |||
let initials1 = initials.toUpperCase(); | |||
if (w.devicePixelRatio) { | |||
size = size * w.devicePixelRatio; | |||
} | |||
LetterAvatar.transform = function() { | |||
Array.prototype.forEach.call(d.querySelectorAll('img[avatar]'), function(img, name, color) { | |||
name = img.getAttribute('avatar'); | |||
color = img.getAttribute('color'); | |||
img.src = LetterAvatar(name, img.getAttribute('width'), color); | |||
img.removeAttribute('avatar'); | |||
img.setAttribute('alt', name); | |||
}); | |||
}; | |||
// AMD support | |||
if (typeof define === 'function' && define.amd) { | |||
define(function () { return LetterAvatar; }); | |||
charIndex = (initials == "?" ? 72 : initials.charCodeAt(0)) - 64; | |||
colourIndex = charIndex % 20; | |||
canvas = d.createElement("canvas"); | |||
canvas.width = size; | |||
canvas.height = size; | |||
context = canvas.getContext("2d"); | |||
context.fillStyle = color ? color : colours[colourIndex - 1]; | |||
context.fillRect(0, 0, canvas.width, canvas.height); | |||
context.font = Math.round(canvas.width / 2) + "px 'Microsoft Yahei'"; | |||
context.textAlign = "center"; | |||
context.fillStyle = "#FFF"; | |||
context.fillText(initials1, size / 2, size / 1.5); | |||
dataURI = canvas.toDataURL(); | |||
canvas = null; | |||
return dataURI; | |||
} | |||
LetterAvatar.transform = function () { | |||
Array.prototype.forEach.call( | |||
d.querySelectorAll("img[avatar]"), | |||
function (img, name, color) { | |||
name = img.getAttribute("avatar"); | |||
color = img.getAttribute("color"); | |||
img.src = LetterAvatar(name, img.getAttribute("width"), color); | |||
img.removeAttribute("avatar"); | |||
img.setAttribute("alt", name); | |||
} | |||
); | |||
}; | |||
// AMD support | |||
if (typeof define === "function" && define.amd) { | |||
define(function () { | |||
return LetterAvatar; | |||
}); | |||
// CommonJS and Node.js module support. | |||
} else if (typeof exports !== 'undefined') { | |||
// Support Node.js specific `module.exports` (which can be a function) | |||
if (typeof module != 'undefined' && module.exports) { | |||
exports = module.exports = LetterAvatar; | |||
} | |||
// But always support CommonJS module 1.1.1 spec (`exports` cannot be a function) | |||
exports.LetterAvatar = LetterAvatar; | |||
} else { | |||
window.LetterAvatar = LetterAvatar; | |||
d.addEventListener('DOMContentLoaded', function(event) { | |||
LetterAvatar.transform(); | |||
}); | |||
} else if (typeof exports !== "undefined") { | |||
// Support Node.js specific `module.exports` (which can be a function) | |||
if (typeof module != "undefined" && module.exports) { | |||
exports = module.exports = LetterAvatar; | |||
} | |||
})(window, document); | |||
// But always support CommonJS module 1.1.1 spec (`exports` cannot be a function) | |||
exports.LetterAvatar = LetterAvatar; | |||
} else { | |||
window.LetterAvatar = LetterAvatar; | |||
d.addEventListener("DOMContentLoaded", function (event) { | |||
LetterAvatar.transform(); | |||
}); | |||
} | |||
})(window, document); |