Browse Source

first commit

Piotr Czajkowski 7 years ago
commit
8c7a06f987
13 changed files with 546 additions and 0 deletions
  1. 18 0
      README.md
  2. 72 0
      app.go
  3. 10 0
      html/error.html
  4. 20 0
      html/index.html
  5. 62 0
      html/languages.json
  6. 34 0
      html/results.html
  7. 34 0
      html/tms.html
  8. 0 0
      log/placeholder
  9. 53 0
      logger.go
  10. 103 0
      search.go
  11. 5 0
      secrets.json
  12. 89 0
      server.go
  13. 46 0
      tm.go

+ 18 - 0
README.md

@@ -0,0 +1,18 @@
+# TM Seach for memoQ Server
+
+This is a proof-of-concept tool (hobby project) which utilizes [memoQ server Resources API](https://www.memoq.com/en/the-memoq-apis/memoq-server-resources-api).
+
+It provides simple HTML interface which of course can be improved. There's also logging mechanism which collects requestor's IP, phrase he was searching for, target language and number of served results. Logs are saved in *log* subfolder in separate *.log* files (one per day) in CSV format.
+
+You just need to build it and make sure that subfolders **html** and **log** are present in the same location as your binary. You'll also need **secrets.json**, just make sure you fill it with proper credentials. Account used needs to be able to list TMs on your server and read their content, of course. It's using only standard GO packages, so there are no external dependencies.
+
+Usage is simple. To get started just launch compiled binary with *-b* switch followed by the URL of your Resources API. Now just navigate to *localhost/* in your browser and start searching your TMs. You may also want to adjust *html/languages.json* to be more relevant to your environment.
+
+Optional parameters are as follows:
+
+- *h* - if you want to serve it under hostname different than *localhost*
+- *p* - if you want to serve it on port different than *80*
+
+You can also navigate to *localhost/tms* to list all your TMs or to *localhost/tms?lang=fre-FR* to list TMs for given language.
+
+**This app was designed to be used on local network or via VPN, so it lacks any security which would be necessary when exposed to Internet. It was also never tested under heavy load. You're free to use it however you wish, but I take no responsibility for any possible damage caused by it.**

+ 72 - 0
app.go

@@ -0,0 +1,72 @@
+package main
+
+import (
+	"bytes"
+	"encoding/json"
+	"io"
+	"io/ioutil"
+	"log"
+	"net/http"
+	"os"
+	"time"
+)
+
+type Application struct {
+	Name, AccessToken, Sid, BaseURL, AuthString string
+	Languages                                   map[string]string
+	Delay                                       time.Duration
+}
+
+func JsonDecoder(data io.ReadCloser, target interface{}) {
+	decoder := json.NewDecoder(data)
+
+	err := decoder.Decode(target)
+	if err != nil {
+		log.Printf("error reading json: %v", err)
+	}
+}
+
+func (app *Application) LoadLanguages() {
+	data, err := os.Open("./html/languages.json")
+	defer data.Close()
+	if err != nil {
+		log.Printf("error reading languages: %v", err)
+		return
+	}
+
+	app.Languages = make(map[string]string)
+	JsonDecoder(data, &app.Languages)
+}
+
+func (app Application) CheckLanguage(language string) bool {
+	_, ok := app.Languages[language]
+	if !ok {
+		return false
+	}
+
+	return true
+}
+
+func (app *Application) Login() {
+	credentials, err := ioutil.ReadFile("./secrets.json")
+	if err != nil {
+		log.Printf("Error reading credentials: %v", err)
+	}
+
+	loginURL := app.BaseURL + "auth/login"
+
+	req, err := http.NewRequest("POST", loginURL, bytes.NewBuffer(credentials))
+	req.Header.Set("Content-Type", "application/json")
+
+	client := &http.Client{}
+	resp, err := client.Do(req)
+	if err != nil {
+		log.Printf("error logging in: %v", err)
+	}
+	defer resp.Body.Close()
+
+	JsonDecoder(resp.Body, &app)
+
+	app.AuthString = "?authToken=" + app.AccessToken
+	log.Println(app.AuthString, resp.Status)
+}

+ 10 - 0
html/error.html

@@ -0,0 +1,10 @@
+<!doctype html>
+<html>
+    <head>
+        <title>memoQ TM Search - error page</title>
+    </head>
+    <body>
+        <h1>{{.}}</h1>
+        <a href="/">Go back to start</a>
+    </body>
+</html>

+ 20 - 0
html/index.html

@@ -0,0 +1,20 @@
+<!doctype html>
+<html>
+    <head>
+        <title>memoQ TM Search</title>
+    </head>
+    <body>
+        <form action="/q">
+            <p>Text:</p>
+            <input type="text" name="phrase">
+            <p>Language:</p>
+            <select name="lang">
+                <option value="">All</option>
+                {{range $key, $value := .}}
+                <option value="{{$key}}">{{$value}}</option>
+                {{end}}
+            </select>
+            <input type="submit" value="Search">
+        </form>
+    </body>
+</html>

+ 62 - 0
html/languages.json

@@ -0,0 +1,62 @@
+{
+  "afr": "Afrikaans",
+  "alb": "Albanian",
+  "ara-EG": "Arabic (Egypt)",
+  "ara-AE": "Arabic",
+  "bel": "Belarusian",
+  "bos": "Bosnian",
+  "bul": "Bulgarian",
+  "zho-SG": "Chinese (Simplified - Singapore)",
+  "zho-CN": "Chinese (Simplified)",
+  "zho-HK": "Chinese (Traditional - Hong Kong)",
+  "zho-TW": "Chinese (Traditional - Taiwan)",
+  "hrv": "Croatian",
+  "cze": "Czech",
+  "dan": "Danish",
+  "dut": "Dutch",
+  "eng-GB": "English (GB)",
+  "eng-US": "English (USA)",
+  "eng": "English",
+  "est": "Estonian",
+  "fin": "Finnish",
+  "dut-BE": "Flemish",
+  "fre-BE": "French (Belgium)",
+  "fre-CA": "French (CA)",
+  "fre-CH": "French (CH)",
+  "fre-FR": "French (France)",
+  "ger-AT": "German (Austria)",
+  "ger-CH": "German (CH)",
+  "ger-DE": "German (Germany)",
+  "gre": "Greek",
+  "heb": "Hebrew",
+  "hin": "Hindi",
+  "hun": "Hungarian",
+  "ice": "Icelandic",
+  "ind": "Indonesian",
+  "ita-IT": "Italian (Italy)",
+  "ita-CH": "Italian (Switzerland)",
+  "jpn": "Japanese",
+  "kor": "Korean",
+  "lav": "Latvian",
+  "lit": "Lithuanian",
+  "mac": "Macedonian",
+  "msa": "Malay",
+  "nnb": "Norwegian (Bokmål)",
+  "pol": "Polish",
+  "por-BR": "Portuguese (Brazil)",
+  "por-PT": "Portuguese (Portugal)",
+  "rum": "Romanian",
+  "rus": "Russian",
+  "scc": "Serbian (Cyrillic)",
+  "scr": "Serbian (Latin)",
+  "slo": "Slovak",
+  "slv": "Slovenian",
+  "spa-AR": "Spanish (Latin America)",
+  "spa-MX": "Spanish (Mexico)",
+  "spa-ES": "Spanish (Spain)",
+  "swe": "Swedish",
+  "tha": "Thai",
+  "tur": "Turkish",
+  "ukr": "Ukrainian",
+  "vie": "Vietnamese"
+}

+ 34 - 0
html/results.html

@@ -0,0 +1,34 @@
+<!doctype html>
+<html>
+    <head>
+        <title>Search results for "{{.SearchPhrase}}"</title>
+        <style>
+            table, th, td {
+                border: 1px solid black;
+                border-collapse: collapse;
+            }
+        </style>
+    </head>
+    <body>
+        <a href="/">Search again</a>
+        {{range .Results}}
+            <h1>Results from {{.TMName}} ({{len .Segments}} results)</h1>
+            <table>
+                <tr style='text-align: left'>
+                    <th>No.</th>
+                    <th>Source</th>
+                    <th>Translation</th>
+                </tr>
+                {{range $i, $segment := .Segments}}
+                    <tr style='text-align: left'>
+                        <td>{{add $i 1}}</td>
+                        <td>{{$segment.Source}}</td>
+                        <td>{{$segment.Target}}</td>
+                    </tr>
+                {{end}}
+            </table>
+            <br/>
+            <a href="/">Search again</a>
+        {{end}}
+    </body>
+</html>

+ 34 - 0
html/tms.html

@@ -0,0 +1,34 @@
+<!doctype html>
+<html>
+    <head>
+        <title>Our TMs</title>
+        <style>
+            table, th, td {
+                border: 1px solid black;
+                border-collapse: collapse;
+            }
+        </style>
+    </head>
+    <body>
+        <a href="/">Go back to start</a>
+        <h1>We have {{len .TMs}} TMs</h1>
+            <table>
+                <tr style='text-align: left'>
+                    <th>Name</th>
+                    <th>Source language</th>
+                    <th>Target language</th>
+                    <th>No. of segments</th>
+                </tr>
+                {{range .TMs}}
+                    <tr style='text-align: left'>
+                        <td><b>{{.FriendlyName}}</b></td>
+                        <td>{{.SourceLangCode}}</td>
+                        <td>{{.TargetLangCode}}</td>
+                        <td>{{.NumEntries}}</td>
+                    </tr>
+                {{end}}
+            </table>
+            <br/>
+            <a href="/">Go back to start</a>
+    </body>
+</html>

+ 0 - 0
log/placeholder


+ 53 - 0
logger.go

@@ -0,0 +1,53 @@
+package main
+
+import (
+	"fmt"
+	"log"
+	"net"
+	"net/http"
+	"os"
+	"path/filepath"
+	"time"
+)
+
+func GetInfoFromRequest(r *http.Request) (string, string, string) {
+	host, _, _ := net.SplitHostPort(r.RemoteAddr)
+	searchPhrase := r.URL.Query().Get("phrase")
+	language := r.URL.Query().Get("lang")
+	if language == "" {
+		language = "All languages"
+	} else {
+		language = app.Languages[language]
+	}
+
+	return host, searchPhrase, language
+}
+
+func WriteLog(logString string) error {
+	logFile := filepath.Join("log", (time.Now().Format("200612") + ".log"))
+	logOutput, err := os.OpenFile(logFile, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666)
+	if err != nil {
+		return err
+	}
+	defer logOutput.Close()
+
+	_, err = logOutput.WriteString(logString)
+	return err
+}
+
+func Logger(r *http.Request, resultsServed int) {
+	host, searchPhrase, language := GetInfoFromRequest(r)
+	timeFormat := "2006-01-02 15:04:05"
+
+	var logString string
+	if searchPhrase != "" {
+		logString = fmt.Sprintf("%v,%v,\"%v\",\"%v\",%v\n", time.Now().Format(timeFormat), host, searchPhrase, language, resultsServed)
+	} else {
+		logString = fmt.Sprintf("%v,%v,TMS,\"%v\",%v\n", time.Now().Format(timeFormat), host, language, resultsServed)
+	}
+
+	err := WriteLog(logString)
+	if err != nil {
+		log.Fatalf("error writing log: %v", err)
+	}
+}

+ 103 - 0
search.go

@@ -0,0 +1,103 @@
+package main
+
+import (
+	"bytes"
+	"log"
+	"net/http"
+	"regexp"
+	"time"
+)
+
+type Segment struct {
+	Source, Target string
+}
+
+func (s *Segment) Clean() {
+	re := regexp.MustCompile("</?seg>")
+	s.Source = re.ReplaceAllString(s.Source, "")
+	s.Target = re.ReplaceAllString(s.Target, "")
+}
+
+type CleanedResults struct {
+	TMName   string
+	Segments []Segment
+}
+
+type SearchResults struct {
+	SearchPhrase string
+	Results      []CleanedResults
+	TotalResults int
+}
+
+type ResultsFromServer struct {
+	ConcResult []struct {
+		ConcordanceTextRanges []struct {
+			Length, Start int
+		}
+		ConcordanceTranslationRanges []string
+		Length, StartPos             int
+		TMEntry                      struct {
+			SourceSegment, TargetSegment string
+		}
+	}
+	ConcTransResult, Errors []string
+	TotalConcResult         int
+}
+
+func PostQuery(requestURL string, searchJSON []byte) *http.Response {
+	req, err := http.NewRequest("POST", requestURL, bytes.NewBuffer(searchJSON))
+	req.Header.Set("Content-Type", "application/json")
+
+	client := &http.Client{}
+	resp, err := client.Do(req)
+	if err != nil {
+		log.Printf("error posting query: %v", err)
+	}
+
+	return resp
+}
+
+func Search(TMs TMList, text string) SearchResults {
+	searchString := "{ \"SearchExpression\": [ \"" + text + "\" ]}"
+	searchJSON := []byte(searchString)
+
+	tmURL := app.BaseURL + "tms/"
+
+	var finalResults SearchResults
+	finalResults.SearchPhrase = text
+
+	var results []CleanedResults
+	for _, tm := range TMs.TMs {
+		getTM := tmURL + tm.TMGuid
+		concordanceURL := getTM + "/concordance"
+		requestURL := concordanceURL + app.AuthString
+
+		resp := PostQuery(requestURL, searchJSON)
+		defer resp.Body.Close()
+		if resp.StatusCode == 401 {
+			time.Sleep(app.Delay)
+			app.Login()
+			return Search(TMs, text)
+		}
+
+		var tempResults ResultsFromServer
+		JsonDecoder(resp.Body, &tempResults)
+
+		if tempResults.TotalConcResult > 0 {
+			var tmResults CleanedResults
+			//Allocating Segments array beforehand
+			tmResults.Segments = make([]Segment, 0, tempResults.TotalConcResult)
+			tmResults.TMName = tm.FriendlyName
+
+			for _, result := range tempResults.ConcResult {
+				segment := Segment{result.TMEntry.SourceSegment, result.TMEntry.TargetSegment}
+				segment.Clean()
+				tmResults.Segments = append(tmResults.Segments, segment)
+			}
+			results = append(results, tmResults)
+			finalResults.TotalResults += len(tmResults.Segments)
+		}
+	}
+	finalResults.Results = results
+	return finalResults
+}

+ 5 - 0
secrets.json

@@ -0,0 +1,5 @@
+{
+    "username": "",
+    "password": "",
+    "LoginMode": "0"
+}

+ 89 - 0
server.go

@@ -0,0 +1,89 @@
+package main
+
+import (
+	"flag"
+	"html/template"
+	"log"
+	"net/http"
+	"time"
+)
+
+var host = flag.String("h", "localhost", "host")
+var port = flag.String("p", "80", "port")
+var url = flag.String("b", "", "API URL")
+var app Application
+var errorPage = template.Must(template.ParseFiles("./html/error.html"))
+
+func ServeIndex(w http.ResponseWriter, r *http.Request) {
+	t := template.Must(template.ParseFiles("./html/index.html"))
+	t.Execute(w, app.Languages)
+}
+
+//Addition for counter
+func add(x, y int) int {
+	return x + y
+}
+
+func DisplaySearchResults(w http.ResponseWriter, r *http.Request) {
+	language := r.URL.Query().Get("lang")
+	searchPhrase := r.URL.Query().Get("phrase")
+
+	if searchPhrase != "" {
+		var searchResults SearchResults
+		if language == "" || app.CheckLanguage(language) {
+			searchResults = Search(GetTMs(language), searchPhrase)
+			Logger(r, searchResults.TotalResults)
+		} else {
+			errorPage.Execute(w, "Language not valid!")
+			return
+		}
+
+		if len(searchResults.Results) > 0 {
+			funcs := template.FuncMap{"add": add}
+			t := template.Must(template.New("results.html").Funcs(funcs).ParseFiles("./html/results.html"))
+			t.Execute(w, searchResults)
+		} else {
+			errorPage.Execute(w, "Nothing found!")
+		}
+	} else {
+		errorPage.Execute(w, "You need to enter search phrase!")
+	}
+}
+
+func DisplayTMs(w http.ResponseWriter, r *http.Request) {
+	language := r.URL.Query().Get("lang")
+
+	var list TMList
+	if language == "" || app.CheckLanguage(language) {
+		list = GetTMs(language)
+		Logger(r, len(list.TMs))
+	} else {
+		errorPage.Execute(w, "Language not valid!")
+		return
+	}
+
+	if len(list.TMs) > 0 {
+		t := template.Must(template.New("tms.html").ParseFiles("./html/tms.html"))
+		t.Execute(w, list)
+	} else {
+		errorPage.Execute(w, "No TMs to display!")
+	}
+}
+
+func main() {
+	flag.Parse()
+	app.BaseURL = *url
+	if app.BaseURL == "" {
+		log.Panicln("Can't do anything without URL to API")
+	}
+
+	app.Login()
+	app.LoadLanguages()
+	app.Delay = time.Duration(20 * time.Second)
+
+	hostname := *host + ":" + *port
+	http.HandleFunc("/", ServeIndex)
+	http.HandleFunc("/q", DisplaySearchResults)
+	http.HandleFunc("/tms", DisplayTMs)
+	log.Fatal(http.ListenAndServe(hostname, nil))
+}

+ 46 - 0
tm.go

@@ -0,0 +1,46 @@
+package main
+
+import (
+	"log"
+	"net/http"
+	"time"
+)
+
+type TMList struct {
+	TMs []struct {
+		NumEntries, AccessLevel                                                                         int
+		Client, Domain, FriendlyName, Project, SourceLangCode, Subject, TMGuid, TMOwner, TargetLangCode string
+	}
+}
+
+func GetQuery(url string) *http.Response {
+	resp, err := http.Get(url)
+	if err != nil {
+		log.Printf("error getting query: %v", err)
+	}
+
+	return resp
+}
+
+func GetTMs(language string) TMList {
+	tmURL := app.BaseURL + "tms/"
+	var queryURL string
+	if language == "" {
+		queryURL = tmURL + app.AuthString
+	} else {
+		queryURL = tmURL + app.AuthString + "&targetLang=" + language
+	}
+
+	resp := GetQuery(queryURL)
+	defer resp.Body.Close()
+	if resp.StatusCode == 401 {
+		time.Sleep(app.Delay)
+		app.Login()
+		return GetTMs(language)
+	}
+
+	var results TMList
+	JsonDecoder(resp.Body, &results.TMs)
+
+	return results
+}