├── LICENSE.txt ├── README.md ├── cmd.go ├── emoji.go ├── emoji_test.go ├── go.mod ├── go.sum ├── main.go ├── prompt.go ├── tar.go ├── umask.go └── umask_windows.go /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # emojidownloader 2 | Mastodon Emoji Downloader 3 | 4 | 5 | 1.自行编译,在go环境中直接 go build 即可。 6 | 7 | 8 | 2.使用已编译程序 9 | 10 | 在[release](https://github.com/Starainrt/emojidownloader/releases)界面按操作系统下载编译好的二进制文件运行即可。 11 | -------------------------------------------------------------------------------- /cmd.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "regexp" 7 | "strings" 8 | 9 | "b612.me/starlog" 10 | "github.com/spf13/cobra" 11 | ) 12 | 13 | var emo = NewEmojis() 14 | var regOld, regNew, regFilter string 15 | var allowCate []string 16 | var useJson bool 17 | 18 | func init() { 19 | cmdRoot.AddCommand(cmdMain, cmdShow) 20 | cmdMain.Flags().StringVarP(&emo.SaveFolders, "savepath", "s", "./myEmojis", "emoji下载文件夹") 21 | cmdMain.Flags().StringVarP(&emo.AuthCookie, "cookie", "c", "", "认证cookie,填入_session_id的值") 22 | cmdMain.Flags().BoolVarP(&emo.IgnoreErr, "ignore-error", "i", false, "忽略下载错误") 23 | cmdMain.Flags().BoolVarP(&emo.Zip2Tarfile, "zip", "z", true, "压缩为tar.gz文件") 24 | cmdMain.Flags().BoolVarP(&emo.DeletedOriginIfZip, "delete-after-zip", "d", false, "压缩为tar.gz文件后删除原始文件") 25 | cmdMain.Flags().StringSliceVarP(&allowCate, "allow-download-category", "a", []string{}, "要下载的分类") 26 | cmdMain.Flags().StringVarP(®Filter, "filter", "f", "", "emoji名称白名单正则表达式") 27 | cmdMain.Flags().StringVarP(®Old, "replace-old", "o", "", "emoji名称替旧名称正则表达式") 28 | cmdMain.Flags().StringVarP(®New, "replace-new", "r", "", "emoji名称替换新字符串") 29 | cmdMain.Flags().IntVarP(&emo.Threads, "threads", "n", 4, "同时下载协程数") 30 | cmdMain.Flags().StringVarP(&emo.Proxy, "proxy", "p", "", "使用代理") 31 | cmdRoot.PersistentFlags().BoolVarP(&useJson, "use-json-file", "j", false, "使用json文件而不是url") 32 | } 33 | 34 | var cmdRoot = &cobra.Command{ 35 | Use: "get", 36 | Short: "Mastodon Emoji Downloader", 37 | } 38 | 39 | var cmdMain = &cobra.Command{ 40 | Use: "get", 41 | Short: "Mastodon Emoji Downloader", 42 | Run: func(cmd *cobra.Command, args []string) { 43 | if len(args) == 0 { 44 | starlog.Errorln("请输入一个url或文件地址") 45 | os.Exit(1) 46 | } 47 | if regFilter != "" { 48 | rpgF, err := regexp.Compile(regFilter) 49 | if err != nil { 50 | starlog.Errorln("invalid regexp:", regFilter, err) 51 | } 52 | emo.filterRp = rpgF 53 | } 54 | if regOld != "" { 55 | rpgF, err := regexp.Compile(regOld) 56 | if err != nil { 57 | starlog.Errorln("invalid regexp:", regOld, err) 58 | } 59 | emo.rpCodeOld = rpgF 60 | emo.rpNew = regNew 61 | } 62 | if emo.Threads <= 0 { 63 | starlog.Errorln("并发下载数不能小于等于0", emo.Threads) 64 | os.Exit(3) 65 | } 66 | url := strings.ToLower(strings.TrimSpace(args[0])) 67 | if !useJson && strings.Index(url, "https://") != 0 { 68 | url = "https://" + url 69 | } 70 | err := emo.LoadAndParse(url, !useJson) 71 | if err != nil { 72 | starlog.Errorln("load emoji lists failed:", err) 73 | os.Exit(4) 74 | } 75 | if len(allowCate) != 0 { 76 | myMap := emo.CategoryCount() 77 | for _, v := range allowCate { 78 | if _, ok := myMap[v]; !ok { 79 | starlog.Errorln(v, "not in the categroy lists,please check") 80 | os.Exit(5) 81 | } 82 | emo.AllowCategories[v] = true 83 | } 84 | } 85 | err = emo.Download(showFn) 86 | if err != nil { 87 | starlog.Errorln("download failed", err) 88 | } 89 | starlog.Infoln("finished") 90 | }, 91 | } 92 | 93 | var cmdShow = &cobra.Command{ 94 | Use: "category", 95 | Args: cobra.ExactArgs(1), 96 | Short: "Show Mastodon Emoji Categories", 97 | Run: func(cmd *cobra.Command, args []string) { 98 | if len(args) == 0 { 99 | starlog.Errorln("请输入一个url或文件地址") 100 | os.Exit(1) 101 | } 102 | url := strings.ToLower(strings.TrimSpace(args[0])) 103 | if !useJson && strings.Index(url, "https://") != 0 { 104 | url = "https://" + url 105 | } 106 | err := emo.LoadAndParse(url, !useJson) 107 | if err != nil { 108 | starlog.Errorln("load emoji lists failed:", err) 109 | os.Exit(4) 110 | } 111 | ct, orderSlice, allCat := emo.Counts() 112 | fmt.Println("按分类解析结果如下:") 113 | fmt.Printf("%-5s %-10s %-28s\n", "序号", "表情个数", "分类名") 114 | for k, v := range orderSlice { 115 | fmt.Printf("%-5v %-10d %-28s\n", k+1, allCat[v], v) 116 | } 117 | starlog.Green("在%d个分类中共找到%d个表情\n", len(orderSlice), ct) 118 | starlog.Infoln("已完成,保存到", emo.SaveFolders) 119 | }, 120 | } 121 | -------------------------------------------------------------------------------- /emoji.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "io/ioutil" 7 | "os" 8 | "path" 9 | "path/filepath" 10 | "regexp" 11 | 12 | "b612.me/stario" 13 | "b612.me/starnet" 14 | "b612.me/staros" 15 | ) 16 | 17 | type Emoji struct { 18 | ShortCode string `json:"shortcode"` 19 | Url string `json:"url"` 20 | StaticUrl string `json:"static_url"` 21 | Picker bool `json:"visible_in_picker"` 22 | Category string `json:"category"` 23 | } 24 | 25 | type EmojiOpt func(*EmojiOpts) 26 | 27 | type EmojiOpts struct { 28 | Zip2Tarfile bool 29 | DeletedOriginIfZip bool 30 | IgnoreErr bool 31 | AuthCookie string 32 | filterRp *regexp.Regexp 33 | rpCodeOld *regexp.Regexp 34 | rpNew string 35 | Threads int 36 | SaveFolders string 37 | Proxy string 38 | } 39 | 40 | type Emojis struct { 41 | Lists []Emoji 42 | AllowCategories map[string]bool 43 | zipMaps map[string]*TarFile 44 | OriginJson string 45 | counts map[string]int 46 | allCategories []string 47 | EmojiOpts 48 | } 49 | 50 | func (e *Emojis) Parse() error { 51 | err := json.Unmarshal([]byte(e.OriginJson), &e.Lists) 52 | if err != nil { 53 | return err 54 | } 55 | e.allCategories = []string{} 56 | e.counts = make(map[string]int) 57 | for _, v := range e.Lists { 58 | if v.Category == "" { 59 | v.Category = "未分类" 60 | } 61 | if _, ok := e.counts[v.Category]; !ok { 62 | e.allCategories = append(e.allCategories, v.Category) 63 | } 64 | e.counts[v.Category] = e.counts[v.Category] + 1 65 | } 66 | return nil 67 | } 68 | 69 | func (e *Emojis) replaceName(name string) string { 70 | if e.rpCodeOld == nil { 71 | return name 72 | } 73 | return e.rpCodeOld.ReplaceAllString(name, e.rpNew) 74 | } 75 | 76 | func (e *Emojis) filterName(name string) bool { 77 | if e.filterRp == nil { 78 | return true 79 | } 80 | return e.filterRp.MatchString(name) 81 | } 82 | 83 | func (e *Emojis) generateRequest(url string, data []byte, nettype string) starnet.Request { 84 | req := starnet.NewRequests(url, nil, "GET", starnet.WithProxy(e.Proxy), starnet.WithUserAgent("b612 emoji downloader "+VERSION)) 85 | if e.AuthCookie != "" { 86 | req.AddSimpleCookie("_session_id", e.AuthCookie) 87 | } 88 | return req 89 | } 90 | 91 | func (e *Emojis) Download(fns ...func(v Emoji, finished bool, err error)) error { 92 | SetUmask(0) 93 | defer UnsetUmask() 94 | var fn func(v Emoji, finished bool, err error) = nil 95 | if len(fns) != 0 { 96 | fn = fns[0] 97 | } 98 | download := func(d Emoji) error { 99 | if fn != nil { 100 | fn(d, false, nil) 101 | } 102 | savePath := filepath.Join(e.SaveFolders, d.Category) 103 | if !staros.Exists(d.Category) { 104 | err := os.MkdirAll(savePath, 0755) 105 | if err != nil { 106 | fn(d, false, err) 107 | return err 108 | } 109 | } 110 | req := e.generateRequest(d.Url, nil, "GET") 111 | data, err := starnet.Curl(req) 112 | if err != nil { 113 | fn(d, false, err) 114 | return err 115 | } 116 | dstPath := filepath.Join(savePath, e.replaceName(d.ShortCode)+path.Ext(d.Url)) 117 | err = ioutil.WriteFile(dstPath, data.RecvData, 0644) 118 | if e.Zip2Tarfile { 119 | var err error 120 | if _, ok := e.zipMaps[d.Category]; !ok { 121 | e.zipMaps[d.Category], err = NewTar(filepath.Join(e.SaveFolders, d.Category+".tar.gz")) 122 | if err != nil { 123 | delete(e.zipMaps, d.Category) 124 | fn(d, false, err) 125 | return err 126 | } 127 | } 128 | e.zipMaps[d.Category].AddFile(dstPath, filepath.Base(dstPath)) 129 | } 130 | if fn != nil { 131 | fn(d, true, err) 132 | } 133 | return err 134 | } 135 | wg := stario.NewWaitGroup(e.Threads) 136 | errChan := make(chan error, 1) 137 | var err error 138 | exitfor: 139 | for _, v := range e.Lists { 140 | if v.Category == "" { 141 | v.Category = "未分类" 142 | } 143 | if len(e.AllowCategories) == 0 || e.AllowCategories[v.Category] { 144 | if !e.filterName(v.ShortCode) { 145 | continue 146 | } 147 | t := v 148 | select { 149 | case err = <-errChan: 150 | break exitfor 151 | default: 152 | } 153 | wg.Add(1) 154 | go func() { 155 | defer wg.Done() 156 | if err == nil { 157 | if err := download(t); err != nil && !e.IgnoreErr { 158 | errChan <- err 159 | } 160 | } 161 | }() 162 | } 163 | } 164 | wg.Wait() 165 | for k, v := range e.zipMaps { 166 | v.Finish() 167 | if k != "" && e.DeletedOriginIfZip { 168 | os.RemoveAll(filepath.Join(e.SaveFolders, k)) 169 | } 170 | } 171 | return err 172 | } 173 | 174 | func (e *Emojis) Load(fpath string, isSite bool) error { 175 | if isSite { 176 | fpath += "/api/v1/custom_emojis" 177 | data, err := starnet.Curl(e.generateRequest(fpath, nil, "GET")) 178 | if err != nil { 179 | return err 180 | } 181 | if data.RespHttpCode != 200 { 182 | return fmt.Errorf("http code not correct! got %v,resp text is %v", data.RespHttpCode, string(data.RecvData)) 183 | } 184 | e.OriginJson = string(data.RecvData) 185 | return nil 186 | } 187 | data, err := ioutil.ReadFile(fpath) 188 | if err != nil { 189 | return err 190 | } 191 | e.OriginJson = string(data) 192 | return nil 193 | } 194 | 195 | func (e *Emojis) LoadAndParse(fpath string, isSite bool) error { 196 | if err := e.Load(fpath, isSite); err != nil { 197 | return err 198 | } 199 | return e.Parse() 200 | } 201 | 202 | func (e *Emojis) CategoryCount() map[string]int { 203 | if e.counts == nil { 204 | return map[string]int{} 205 | } 206 | return e.counts 207 | } 208 | 209 | func (e *Emojis) SetDownloadCategories(cates ...string) { 210 | if e.AllowCategories == nil { 211 | e.AllowCategories = make(map[string]bool) 212 | } 213 | for _, v := range cates { 214 | e.AllowCategories[v] = true 215 | } 216 | } 217 | 218 | func (e *Emojis) SetFilter(filter string) error { 219 | if filter == "" { 220 | e.filterRp = nil 221 | return nil 222 | } 223 | regp, err := regexp.Compile(filter) 224 | if err != nil { 225 | return err 226 | } 227 | e.filterRp = regp 228 | return nil 229 | } 230 | 231 | func (e *Emojis) SetReplace(rp string, rpnew string) error { 232 | e.rpNew = rpnew 233 | if rp == "" { 234 | e.rpCodeOld = nil 235 | return nil 236 | } 237 | regp, err := regexp.Compile(rp) 238 | if err != nil { 239 | return err 240 | } 241 | e.rpCodeOld = regp 242 | return nil 243 | } 244 | 245 | func (e *Emojis) Counts() (int, []string, map[string]int) { 246 | return len(e.Lists), e.allCategories, e.counts 247 | } 248 | 249 | func NewEmojis(opt ...EmojiOpt) *Emojis { 250 | defaultEmojis := &Emojis{ 251 | AllowCategories: make(map[string]bool), 252 | zipMaps: make(map[string]*TarFile), 253 | counts: make(map[string]int), 254 | EmojiOpts: EmojiOpts{ 255 | Threads: 1, 256 | SaveFolders: "./myEmojis", 257 | }, 258 | } 259 | for _, v := range opt { 260 | v(&defaultEmojis.EmojiOpts) 261 | } 262 | return defaultEmojis 263 | } 264 | 265 | func WithAuthCookie(cookieValue string) EmojiOpt { 266 | return func(eo *EmojiOpts) { 267 | eo.AuthCookie = cookieValue 268 | } 269 | } 270 | 271 | func WithProxy(p string) EmojiOpt { 272 | return func(eo *EmojiOpts) { 273 | eo.Proxy = p 274 | } 275 | } 276 | 277 | func WithZipFile(z bool) EmojiOpt { 278 | return func(eo *EmojiOpts) { 279 | eo.Zip2Tarfile = z 280 | } 281 | } 282 | 283 | func WithSaveFolder(f string) EmojiOpt { 284 | return func(eo *EmojiOpts) { 285 | eo.SaveFolders = f 286 | } 287 | } 288 | -------------------------------------------------------------------------------- /emoji_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | ) 7 | 8 | func Test_ParseEmojiFromUrl(t *testing.T) { 9 | url := "https://b612.icu" 10 | emo := NewEmojis() 11 | err := emo.LoadAndParse(url, true) 12 | if err != nil { 13 | fmt.Println(err) 14 | t.FailNow() 15 | } 16 | if len(emo.counts)==0 { 17 | t.FailNow() 18 | } 19 | fmt.Println(emo.counts) 20 | fmt.Println(emo.allCategories) 21 | } 22 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/Starainrt/emojidownloader 2 | 3 | go 1.16 4 | 5 | require ( 6 | b612.me/stario v0.0.7 7 | b612.me/starlog v1.3.0 8 | b612.me/starnet v0.1.4 9 | b612.me/staros v1.1.4 10 | github.com/spf13/cobra v1.4.0 11 | golang.org/x/crypto v0.0.0-20220315160706-3147a52a75dd 12 | ) 13 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | b612.me/notify v1.2.0 h1:RedXNMLqY+TozalmdIUM27EFvZp06pzeqHn/F9G1eEs= 2 | b612.me/notify v1.2.0/go.mod h1:EPctpKCVnoZO1hUJTRYpOw3huTemua+SGNIuUCsnzOc= 3 | b612.me/starcrypto v0.0.1 h1:xGngzXPUrVbqtWzNw2e+0eWsdG7GG1/X+ONDGIzdriI= 4 | b612.me/starcrypto v0.0.1/go.mod h1:hz0xRnfWNpYOlVrIPoGrQOWPibq4YiUZ7qN5tsQbzPo= 5 | b612.me/stario v0.0.5/go.mod h1:or4ssWcxQSjMeu+hRKEgtp0X517b3zdlEOAms8Qscvw= 6 | b612.me/stario v0.0.6 h1:XUqmGKBoesrscMFcItvsUCvvy7JMsd2a6hapl+vtggQ= 7 | b612.me/stario v0.0.6/go.mod h1:or4ssWcxQSjMeu+hRKEgtp0X517b3zdlEOAms8Qscvw= 8 | b612.me/stario v0.0.7 h1:QbQcsHCVLE6vRgVrPN4+9DGiSaC6IWdtm4ClL2tpMUg= 9 | b612.me/stario v0.0.7/go.mod h1:or4ssWcxQSjMeu+hRKEgtp0X517b3zdlEOAms8Qscvw= 10 | b612.me/starlog v1.3.0 h1:GV/qhZ1MssUWedT5YHDltGbq+ZUoB58ysNY/yI7QuDw= 11 | b612.me/starlog v1.3.0/go.mod h1:qydvFLzkSg+2TrgNvc+bbx5qC6GaH+dtJUjgQjRL0ro= 12 | b612.me/starmap v1.2.0 h1:sRUeMRUqOyb3pAQln5U6V07kIYp0714Z3gJ/g2nCJXc= 13 | b612.me/starmap v1.2.0/go.mod h1:InIJXA3qVeMkvkUhCV/XPchCiNcJcVYdYV8EAOGbGZY= 14 | b612.me/starnet v0.1.3/go.mod h1:j/dd6BKwQK80O4gfbGYg2aYtPH76gSdgpuKboK/DwN4= 15 | b612.me/starnet v0.1.4 h1:3ptCKE+lVVJv9FxKXz2FDXe+qTjfRQIoxDSMd2iTWUo= 16 | b612.me/starnet v0.1.4/go.mod h1:j/dd6BKwQK80O4gfbGYg2aYtPH76gSdgpuKboK/DwN4= 17 | b612.me/staros v1.1.2 h1:jBFU1KHgn0VpyitYKwC9Zwj1GjmDBGM+fuTyV0yGXRo= 18 | b612.me/staros v1.1.2/go.mod h1:9kNWVJWNJfs2MiWEt7X3SO+ixYKPGqus1ShTy8hpfU0= 19 | b612.me/staros v1.1.4 h1:Ikh74tYMqXkDHXJHArVf1/yhLMORfwZ+q8clAKvYjrM= 20 | b612.me/staros v1.1.4/go.mod h1://P/Ivz7hb/lrI+FwMh5G/T27iJ8WlWZZr3wOoPfVsU= 21 | b612.me/win32api v0.0.1 h1:vLFB1xhO6pd9+zB2EyaapKB459Urv3v+C1YwgwOFEWo= 22 | b612.me/win32api v0.0.1/go.mod h1:MHu0JBQjzxQ2yxpZPUBbn5un45o67eF5iWKa4Q9e0yE= 23 | b612.me/wincmd v0.0.1 h1:4+RCFKHuD/JqAYsdtO6sTNKJs1nQVMQo87h6KhTJjkM= 24 | b612.me/wincmd v0.0.1/go.mod h1:32xTM7qWAI7jx6qwTrig05rxejSYbSp7CX5WD7qsMxY= 25 | github.com/cpuguy83/go-md2man/v2 v2.0.1/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 26 | github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= 27 | github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= 28 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 29 | github.com/spf13/cobra v1.4.0 h1:y+wJpx64xcgO1V+RcnwW0LEHxTKRi2ZDPSBjWnrg88Q= 30 | github.com/spf13/cobra v1.4.0/go.mod h1:Wo4iy3BUC+X2Fybo0PDqwJIv3dNRiZLHQymsfxlB84g= 31 | github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= 32 | github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 33 | golang.org/x/crypto v0.0.0-20220313003712-b769efc7c000 h1:SL+8VVnkqyshUSz5iNnXtrBQzvFF2SkROm6t5RczFAE= 34 | golang.org/x/crypto v0.0.0-20220313003712-b769efc7c000/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= 35 | golang.org/x/crypto v0.0.0-20220315160706-3147a52a75dd h1:XcWmESyNjXJMLahc3mqVQJcgSTDxFxhETVlfk9uGc38= 36 | golang.org/x/crypto v0.0.0-20220315160706-3147a52a75dd/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= 37 | golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 38 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 39 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 40 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 41 | golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5 h1:y/woIyUBFbpQGKS0u1aHF/40WUDnek3fPOyD08H5Vng= 42 | golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 43 | golang.org/x/sys v0.0.0-20220318055525-2edf467146b5 h1:saXMvIOKvRFwbOMicHXr0B1uwoxq9dGmLe5ExMES6c4= 44 | golang.org/x/sys v0.0.0-20220318055525-2edf467146b5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 45 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 h1:v+OssWQX+hTHEmOBgwxdZxK4zHq3yOs8F9J7mk0PY8E= 46 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 47 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 h1:JGgROgKl9N8DuW20oFS5gxc+lE67/N3FcwmBPMe7ArY= 48 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 49 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 50 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 51 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 52 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 53 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | "runtime" 6 | 7 | "golang.org/x/crypto/ssh/terminal" 8 | ) 9 | 10 | const VERSION = "v0.1.0" 11 | 12 | func main() { 13 | if len(os.Args) == 1 && (terminal.IsTerminal(0) || runtime.GOOS == "windows") { 14 | PromotMode() 15 | return 16 | } 17 | cmdRoot.Execute() 18 | } 19 | -------------------------------------------------------------------------------- /prompt.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "regexp" 7 | "strings" 8 | 9 | "b612.me/stario" 10 | "b612.me/starlog" 11 | ) 12 | 13 | var showFn = func(v Emoji, finished bool, err error) { 14 | if err != nil { 15 | starlog.Errorf("下载出错 分类:%-10s emoji:%s error:%v\n", v.Category, v.ShortCode, err) 16 | return 17 | } 18 | if !finished { 19 | starlog.Noticef("开始下载 分类:%-10s emoji:%s \n", v.Category, v.ShortCode) 20 | return 21 | } 22 | starlog.Infof("下载完成 分类:%-10s emoji:%s \n", v.Category, v.ShortCode) 23 | } 24 | 25 | func PromotMode() { 26 | bunny() 27 | starlog.SetLevelColor(starlog.LvNotice, []starlog.Attr{starlog.FgHiYellow}) 28 | emo := NewEmojis() 29 | checkFn := func(fn func(emo *Emojis) int) { 30 | if code := fn(emo); code != 0 { 31 | stario.StopUntil("出现错误,按任意键结束", "", true) 32 | os.Exit(code) 33 | } 34 | } 35 | for _, v := range []func(*Emojis) int{plParseJson, plGetDownloadChoice, plRegexp, plDownload} { 36 | checkFn(v) 37 | } 38 | stario.StopUntil("下载完毕,按任意键退出", "", true) 39 | } 40 | 41 | func plParseJson(emo *Emojis) int { 42 | var fromSite bool 43 | var fpath string 44 | fmt.Println("mastodon emoji downloader " + VERSION) 45 | fmt.Println("当前为交互模式,如果您想使用命令行模式,请执行--help查看用法\n\n") 46 | fmt.Println("今天您想要来点什么?") 47 | fmt.Println("1.从url下载mastodon emoji\n") 48 | fmt.Println("2.从json下载mastodon emoji\n") 49 | for { 50 | choice := stario.MessageBox("请输入您的选择:", "0").MustInt() 51 | if choice != 1 && choice != 2 { 52 | starlog.Red("请输入1或者2哦,您的输入为%d\n", choice) 53 | continue 54 | } 55 | if choice == 1 { 56 | fromSite = true 57 | } 58 | break 59 | } 60 | for { 61 | if fromSite { 62 | fmt.Print("请输入Mastodon域名,不带https:") 63 | } else { 64 | fmt.Print("请输入Json文件地址:") 65 | } 66 | fpath = stario.MessageBox("", "").MustString() 67 | if fpath == "" { 68 | starlog.Red("请实际输入哦") 69 | continue 70 | } 71 | break 72 | } 73 | if fromSite { 74 | if strings.Index(fpath, "https://") != 0 { 75 | fpath = "https://" + fpath 76 | } 77 | if stario.YesNo("是否使用代理?(y/N)", false) { 78 | emo.Proxy = stario.MessageBox("请输入代理地址:", "").MustString() 79 | } 80 | if stario.YesNo("是否设置Mastodon Cookie?(y/N)", false) { 81 | emo.AuthCookie = stario.MessageBox("请输入_session_id Cookie的值:", "").MustString() 82 | } 83 | } 84 | starlog.Infoln("解析中......") 85 | err := emo.LoadAndParse(fpath, fromSite) 86 | if err != nil { 87 | starlog.Errorln("解析失败!请检查", err) 88 | return 1 89 | } 90 | return 0 91 | } 92 | 93 | func plGetDownloadChoice(emo *Emojis) int { 94 | ct, orderSlice, allCat := emo.Counts() 95 | fmt.Println("按分类解析结果如下:") 96 | fmt.Printf("%-5s %-10s %-28s\n", "序号", "表情个数", "分类名") 97 | for k, v := range orderSlice { 98 | fmt.Printf("%-5v %-10d %-28s\n", k+1, allCat[v], v) 99 | } 100 | starlog.Green("在%d个分类中共找到%d个表情\n", len(orderSlice), ct) 101 | exitfor: 102 | for { 103 | choice, err := stario.MessageBox("请输入您要下载的分类序号,如需下载多个分类,用英文逗号分隔多个序号,下载全部表情直接回车:", "0").SliceInt(",") 104 | if err != nil { 105 | starlog.Errorln("您的输入有误,请输入数字,用英文逗号分隔,请检查后重新输入", err) 106 | continue 107 | } 108 | for _, v := range choice { 109 | if v == 0 { 110 | emo.AllowCategories = make(map[string]bool) 111 | fmt.Println("准备下载:全部分类") 112 | break exitfor 113 | } 114 | emo.AllowCategories[orderSlice[v-1]] = true 115 | fmt.Println("准备下载:", orderSlice[v-1]) 116 | } 117 | break 118 | } 119 | return 0 120 | } 121 | 122 | func plRegexp(emo *Emojis) int { 123 | if stario.YesNo("是否开启白名单emoji名称?(y/N)", false) { 124 | for { 125 | rgpR, err := regexp.Compile(stario.MessageBox("请输入白名单正则表达式:", "").MustString()) 126 | if err != nil { 127 | starlog.Errorln("正则表达式不正确", err) 128 | continue 129 | } 130 | emo.filterRp = rgpR 131 | break 132 | } 133 | } 134 | if stario.YesNo("是否替换emoji名称?(y/N)", false) { 135 | for { 136 | rgpR, err := regexp.Compile(stario.MessageBox("请输入匹配emoji正则表达式:", "").MustString()) 137 | if err != nil { 138 | starlog.Errorln("正则表达式不正确", err) 139 | continue 140 | } 141 | emo.rpCodeOld = rgpR 142 | break 143 | } 144 | emo.rpNew = stario.MessageBox("请输入替换后的新名称:", "").MustString() 145 | } 146 | return 0 147 | } 148 | 149 | func plDownload(emo *Emojis) int { 150 | emo.SaveFolders = stario.MessageBox("请输入保存文件夹(默认:./myEmojis):", "./myEmojis").MustString() 151 | emo.IgnoreErr = stario.YesNo("是否忽略下载中的单个emoji下载错误?(Y/n)", true) 152 | emo.Zip2Tarfile = stario.YesNo("是否打包为压缩文件?(Y/n)", true) 153 | if emo.Zip2Tarfile { 154 | emo.DeletedOriginIfZip = stario.YesNo("是否在打包为压缩文件后删除原始的下载文件(Y/n)", true) 155 | } 156 | for { 157 | emo.Threads = stario.MessageBox("并发下载量(默认:16):", "16").MustInt() 158 | if emo.Threads <= 0 { 159 | starlog.Red("输入非法", emo.Threads) 160 | continue 161 | } 162 | break 163 | } 164 | var fn func(v Emoji, finished bool, err error) = nil 165 | if stario.YesNo("是否显示下载日志?(Y/n)", true) { 166 | fn = showFn 167 | } 168 | starlog.Infoln("开始下载...") 169 | err := emo.Download(fn) 170 | if err != nil { 171 | starlog.Errorln("下载失败:", err) 172 | return 1 173 | } 174 | starlog.Infoln("下载成功!保存到", emo.SaveFolders) 175 | fmt.Println(`tips:一键导入表情: 176 | 非docker: 177 | RAILS_ENV=production bin/tootctl emoji import --category=自定义表情分类名 表情tar.gz文件地址 178 | 179 | docker: 180 | docker cp ./表情地址 web服务docker名:/tmp/表情名.tar.gz 181 | docker-compose exec web bin/tootctl emoji import --category=自定义表情分类名 /tmp/表情名.tar.gz`) 182 | return 0 183 | } 184 | 185 | func bunny() { 186 | str := ` 187 | 188 | 189 | WMMMMa rMMMMM: 190 | MM ,MMi MM7 rM. 191 | M 7MX ;MZ Mr 192 | MB .MX rM; 7M 193 | .M :M: iM: M; 194 | aM SM M7 MZ 195 | WM M@ M0 M8 196 | WM MMM ,MS 197 | 0M, XM SMi 198 | ;aW@BMZ MMBW0S: 199 | aMM@Z; ,70MMW7 200 | 7MM0; X@MB, 201 | XMM7 ZMM, 202 | ,MM: . ;, ;: ,,,. 2MW 203 | aMS .ii;i;ii. XMM ii:,MM .i;i;i;ii. MM, 204 | BM :;ii:i:ii;: ;..MMM ii .;;:i:i:ii;, SMi 205 | ZM .;ii:i:i:iir 8 :;:i:::::ii; rM 206 | M. .;i:i:i:i:i; 0 .;i:i:i:::;i 0M 207 | MM :;;iiiii;;, W ,i;iiiii;: Mi 208 | M; ,iiiii, .B .::::, M@ 209 | M: B MM 210 | MZ 0 MS 211 | rM .S ;Xaaa0@MMMMMMMMMMMMMMMMMMMMMMMMMMMM0ZZZX,.2 ;M 212 | MM ;8M0Z8MMMMMMWMa;7r7rr;;i;;r;rr7;MM8BW0MMMZ8MB8 Mr 213 | MM rM 0M Ma.:iiii;i;i;i;ii:.MX ,Mr .M iMS 214 | aMrM8 SM; XM.ii;;r;r;r;;;;ii:M BM. M88M: 215 | 8MMa :MB M0.;;r;;;;;;;;;i,MZ MM MMMS 216 | ;MW MM MW:ii;;;;;i;i:;MW 7M8 MM. 217 | MM SM; BM0Xii:iii;2WMS BM, rM8 218 | 8M8 aWM@@W@@MBX MMi 219 | .MM, ,:. 7M8 220 | 221 | ` 222 | fmt.Println(str) 223 | } 224 | -------------------------------------------------------------------------------- /tar.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "archive/tar" 5 | "compress/gzip" 6 | "io" 7 | "os" 8 | "sync" 9 | ) 10 | 11 | type TarFile struct { 12 | gw *gzip.Writer 13 | tw *tar.Writer 14 | destPath string 15 | fw *os.File 16 | mu sync.Mutex 17 | } 18 | 19 | func NewTar(dstPath string) (*TarFile, error) { 20 | var res TarFile 21 | var err error 22 | res.fw, err = os.Create(dstPath) 23 | if err != nil { 24 | return nil, err 25 | } 26 | res.gw = gzip.NewWriter(res.fw) 27 | res.tw = tar.NewWriter(res.gw) 28 | res.destPath = dstPath 29 | return &res, nil 30 | } 31 | 32 | func (tf *TarFile) AddFile(path, name string) error { 33 | tf.mu.Lock() 34 | defer tf.mu.Unlock() 35 | fso, err := os.Open(path) 36 | if err != nil { 37 | return err 38 | } 39 | defer fso.Close() 40 | stats, err := fso.Stat() 41 | if err != nil { 42 | return err 43 | } 44 | hdr, err := tar.FileInfoHeader(stats, "") 45 | if err != nil { 46 | return err 47 | } 48 | hdr.Name = name 49 | err = tf.tw.WriteHeader(hdr) 50 | if err != nil { 51 | return err 52 | } 53 | _, err = io.Copy(tf.tw, fso) 54 | return err 55 | } 56 | 57 | func (tf *TarFile) Finish() error { 58 | err := tf.tw.Close() 59 | if err != nil { 60 | return err 61 | } 62 | err = tf.gw.Close() 63 | if err != nil { 64 | return err 65 | } 66 | return tf.fw.Close() 67 | } 68 | -------------------------------------------------------------------------------- /umask.go: -------------------------------------------------------------------------------- 1 | //+build !windows 2 | 3 | package main 4 | 5 | import ( 6 | "sync" 7 | "syscall" 8 | ) 9 | 10 | var umask int 11 | var mu sync.Mutex 12 | 13 | func SetUmask(mask int) { 14 | mu.Lock() 15 | defer mu.Unlock() 16 | umask = syscall.Umask(0) 17 | } 18 | 19 | func UnsetUmask() { 20 | mu.Lock() 21 | defer mu.Unlock() 22 | syscall.Umask(umask) 23 | } 24 | -------------------------------------------------------------------------------- /umask_windows.go: -------------------------------------------------------------------------------- 1 | //+build windows 2 | 3 | package main 4 | 5 | func SetUmask(mask int) { 6 | 7 | } 8 | 9 | func UnsetUmask() { 10 | 11 | } 12 | --------------------------------------------------------------------------------