@@ -16,8 +16,6 @@ import com.demonwav.mcdev.util.fromJson
1616import  com.google.gson.Gson 
1717import  com.intellij.ide.plugins.PluginManagerCore 
1818import  com.intellij.openapi.diagnostic.Attachment 
19- import  com.intellij.util.io.readCharSequence 
20- import  java.io.InputStreamReader 
2119import  java.net.HttpURLConnection 
2220import  java.nio.ByteBuffer 
2321import  java.nio.charset.CodingErrorAction 
@@ -29,7 +27,8 @@ object AnonymousFeedback {
2927
3028    data class  FeedbackData (val  url :  String , val  token :  Int , val  isDuplicate :  Boolean )
3129
32-     const  val  url =  " https://www.denwav.dev/errorReport" 
30+     private  const  val  authedUrl =  " https://www.denwav.dev/errorReport" 
31+     private  const  val  baseUrl =  " https://api.github.com/repos/minecraft-dev/MinecraftDev/issues" 
3332
3433    fun  sendFeedback (
3534        factory :  HttpConnectionFactory ,
@@ -39,8 +38,8 @@ object AnonymousFeedback {
3938        val  duplicateId =  findDuplicateIssue(envDetails, factory)
4039        if  (duplicateId !=  null ) {
4140            //  This is a duplicate
42-             val  commentUrl  = 
43-                  sendCommentOnDuplicateIssue(duplicateId, factory, convertToGitHubIssueFormat(envDetails, attachments) )
41+             val  issueContent  =  convertToGitHubIssueFormat(envDetails, attachments) 
42+             val  commentUrl  =   sendCommentOnDuplicateIssue(duplicateId, factory, issueContent )
4443            return  FeedbackData (commentUrl, duplicateId, true )
4544        }
4645
@@ -67,21 +66,31 @@ object AnonymousFeedback {
6766        }
6867
6968        var  stackTrace =  body.remove(" error.stacktrace" 
70-         if  (stackTrace.isNullOrEmpty()) {
71-             stackTrace =  " no stacktrace" 
69+         stackTrace =  if  (stackTrace.isNullOrEmpty()) {
70+             " no stacktrace" 
71+         } else  {
72+             linkStacktrace(stackTrace)
7273        }
7374
7475        val  sb =  StringBuilder ()
7576
76-         if  (! errorDescription.isEmpty ()) {
77+         if  (errorDescription.isNotEmpty ()) {
7778            sb.append(errorDescription).append(" \n\n " 
7879        }
7980
80-         for  ((key, value) in  body) {
81-             sb.append(key).append(" : " " \n " 
81+         sb.append(" <table><tr><td><table>\n " 
82+         for  ((i, entry) in  body.entries.withIndex()) {
83+             if  (i ==  6 ) {
84+                 sb.append(" </table></td><td><table>\n " 
85+             }
86+             val  (key, value) =  entry
87+             sb.append(" <tr><td><b>" " </b></td><td><code>" 
88+                 " </code></td></tr>\n " 
89+             )
8290        }
91+         sb.append(" </table></td></tr></table>\n " 
8392
84-         sb.append(" \n ``` \n " " \n ``` \n " 
93+         sb.append(" \n <pre> \n " " \n </pre> \n " 
8594        sb.append(" \n ```\n " " \n ```\n " 
8695
8796        if  (attachments.isNotEmpty()) {
@@ -118,7 +127,7 @@ object AnonymousFeedback {
118127    }
119128
120129    private  fun  sendFeedback (factory :  HttpConnectionFactory , payload :  ByteArray ): Pair <String , Int > {
121-         val  connection =  getConnection(factory, url )
130+         val  connection =  getConnection(factory, authedUrl )
122131        connection.connect()
123132        val  json =  executeCall(connection, payload)
124133        return  json[" html_url" as  String  to (json[" number" as  Double ).toInt()
@@ -131,17 +140,15 @@ object AnonymousFeedback {
131140        return  connection
132141    }
133142
134-     private  val  numberRegex =  Regex (" \\ d+" 
135-     private  val  newLineRegex =  Regex (" [\r\n ]+" 
136- 
137-     private  const  val  openIssueUrl =  " https://api.github.com/repos/minecraft-dev/MinecraftDev/issues" + 
138-         " ?state=open&creator=minecraft-dev-autoreporter&per_page=100" 
139-     private  const  val  closedIssueUrl =  " https://api.github.com/repos/minecraft-dev/MinecraftDev/issues" + 
140-         " ?state=closed&creator=minecraft-dev-autoreporter&per_page=100" 
143+     private  const  val  openIssueUrl =  " $baseUrl ?state=open&creator=minecraft-dev-autoreporter&per_page=100" 
144+     private  const  val  closedIssueUrl =  " $baseUrl ?state=closed&creator=minecraft-dev-autoreporter&per_page=100" 
141145
142146    private  const  val  packagePrefix =  " \t at com.demonwav.mcdev" 
143147
144148    private  fun  findDuplicateIssue (envDetails :  LinkedHashMap <String , String ?>, factory :  HttpConnectionFactory ): Int?  {
149+         val  numberRegex =  Regex (" \\ d+" 
150+         val  newLineRegex =  Regex (" [\r\n ]+" 
151+ 
145152        val  stack =  envDetails[" error.stacktrace" " " ? :  return  null 
146153
147154        val  stackMcdevParts =  stack.lineSequence()
@@ -177,55 +184,50 @@ object AnonymousFeedback {
177184    }
178185
179186    private  fun  getAllIssues (url :  String , factory :  HttpConnectionFactory ): List <Map <* , * >>?  {
180-         var  connection =  connect(factory, url)
181-         connection.requestMethod =  " GET" 
182-         connection.setRequestProperty(" User-Agent" 
183- 
184-         connection.connect()
185-         if  (connection.responseCode !=  200 ) {
186-             connection.disconnect()
187-             return  null 
188-         }
187+         var  useAuthed =  false 
189188
189+         var  next:  String?  =  url
190190        val  list =  mutableListOf<Map <* , * >>()
191-         var  data =  connection.inputStream.reader().use(InputStreamReader ::readCharSequence).toString()
192191
193-         var  response =  Gson ().fromJson<List <Map <* , * >>>(data)
194-         list.addAll(response)
192+         while  (next !=  null ) {
193+             val  connection:  HttpURLConnection  =  connect(factory, next)
194+             try  {
195+                 connection.requestMethod =  " GET" 
196+                 connection.setRequestProperty(" User-Agent" 
195197
196-         var  link =  connection.getHeaderField(" Link" 
197-         connection.disconnect()
198+                 connection.connect()
198199
199-         var  next  =  getNextLink(link) 
200-         while  (next  !=   null ) { 
201-             connection  =  connect(factory,  next)
202-             connection.requestMethod  =   " GET " 
203-             connection.setRequestProperty( " User-Agent " , userAgent) 
200+                  if  (connection.responseCode  ==   403   &&   ! useAuthed) { 
201+                     useAuthed  =   true 
202+                     next  =  replaceWithAuth( next)
203+                      continue 
204+                 } 
204205
205-             connection.connect()
206-             if  (connection.responseCode !=  200 ) {
207-                 connection.disconnect()
208-                 continue 
209-             }
206+                 if  (connection.responseCode !=  200 ) {
207+                     return  null 
208+                 }
209+ 
210+                 val  charset =  connection.getHeaderField(HttpHeaders .CONTENT_TYPE )?.let  {
211+                     ContentType .parse(it).charset
212+                 } ? :  Charsets .UTF_8 
210213
211-             val  charset =  connection.getHeaderField(HttpHeaders .CONTENT_TYPE )?.let  {
212-                 ContentType .parse(it).charset
213-             } ? :  Charsets .UTF_8 
214+                 val  data =  connection.inputStream.reader(charset).readText()
214215
215-             data =  connection.inputStream.reader(charset).readText()
216+                 val  response =  Gson ().fromJson<List <Map <* , * >>>(data)
217+                 list.addAll(response)
216218
217-             response =  Gson ().fromJson(data)
218-             list.addAll(response)
219+                 val  link =  connection.getHeaderField(" Link" 
219220
220-             link =  connection.getHeaderField(" Link" 
221-             connection.disconnect()
222-             next =  getNextLink(link)
221+                 next =  getNextLink(link, useAuthed)
222+             } finally  {
223+                 connection.disconnect()
224+             }
223225        }
224226
225227        return  list
226228    }
227229
228-     private  fun  getNextLink (linkHeader :  String? ): String?  {
230+     private  fun  getNextLink (linkHeader :  String? ,  useAuthed :   Boolean ): String?  {
229231        if  (linkHeader ==  null ) {
230232            return  null 
231233        }
@@ -239,14 +241,31 @@ object AnonymousFeedback {
239241            if  (parts.isEmpty()) {
240242                continue 
241243            }
242-             return  parts[0 ].trim().removePrefix(" <" " >" 
244+             val  nextUrl =  parts[0 ].trim().removePrefix(" <" " >" 
245+             if  (! useAuthed) {
246+                 return  nextUrl
247+             }
248+ 
249+             return  replaceWithAuth(nextUrl)
243250        }
244251
245252        return  null 
246253    }
247254
255+     private  fun  replaceWithAuth (url :  String ): String?  {
256+         //  non-authed-API requests are rate limited at 60 / hour / IP
257+         //  authed requests have a rate limit of 5000 / hour / account
258+         //  We don't want to use the authed URL by default since all users would use the same rate limit
259+         //  but it's a good fallback when the non-authed API stops working.
260+         val  index =  url.indexOf(' ?' 
261+         if  (index ==  - 1 ) {
262+             return  null 
263+         }
264+         return  authedUrl +  url.substring(index)
265+     }
266+ 
248267    private  fun  sendCommentOnDuplicateIssue (id :  Int , factory :  HttpConnectionFactory , payload :  ByteArray ): String  {
249-         val  commentUrl =  " $url  /$id /comments" 
268+         val  commentUrl =  " $authedUrl  /$id /comments" 
250269        val  connection =  getConnection(factory, commentUrl)
251270        val  json =  executeCall(connection, payload)
252271        return  json[" html_url" as  String 
@@ -281,6 +300,74 @@ object AnonymousFeedback {
281300        return  connection
282301    }
283302
303+     private  fun  linkStacktrace (stacktrace :  String ): String  {
304+         val  versionRegex =  Regex (""" (?<intellijVersion>\d{4}\.\d)-(?<pluginVersion>\d+\.\d+\.\d+)""" 
305+ 
306+         val  version =  PluginUtil .pluginVersion
307+         val  match =  versionRegex.matchEntire(version) ? :  return  stacktrace
308+ 
309+         val  intellijVersion =  match.groups[" intellijVersion" ? :  return  stacktrace
310+         val  pluginVersion =  match.groups[" pluginVersion" ? :  return  stacktrace
311+ 
312+         val  tag =  " $pluginVersion -$intellijVersion " 
313+ 
314+         //          v                             stack element text                                  v
315+         //       at com.demonwav.mcdev.facet.MinecraftFacet.shouldShowPluginIcon(MinecraftFacet.kt:185)
316+         //  prefix ^        class path ^   ^                           file name ^               ^  ^ line number
317+         val  stackElementRegex =  Regex (
318+             """ (?<prefix>\s+at\s+)""" + 
319+                 """ (?<stackElementText>""" + 
320+                 """ (?<className>com\.demonwav\.mcdev(?:\.\p{javaJavaIdentifierStart}\p{javaJavaIdentifierPart}*)+)""" + 
321+                 """ (?:\.\p{javaJavaIdentifierStart}\p{javaJavaIdentifierPart}*|<(?:cl)?init>)""" + 
322+                 """ \((?<fileName>.*\.\w+):(?<lineNumber>\d+)\)""" + 
323+                 """ )\s*""" 
324+         )
325+ 
326+         val  baseTagUrl =  " https://github.com/minecraft-dev/MinecraftDev/blob/$tag /src/main/kotlin/" 
327+ 
328+         val  sb =  StringBuilder (stacktrace.length *  2 )
329+ 
330+         for  (line in  stacktrace.lineSequence()) {
331+             val  lineMatch =  stackElementRegex.matchEntire(line)
332+             if  (lineMatch ==  null ) {
333+                 sb.append(line).append(' \n ' 
334+                 continue 
335+             }
336+ 
337+             val  prefix =  lineMatch.groups[" prefix" 
338+             val  className =  lineMatch.groups[" className" 
339+             val  fileName =  lineMatch.groups[" fileName" 
340+             val  lineNumber =  lineMatch.groups[" lineNumber" 
341+             val  stackElementText =  lineMatch.groups[" stackElementText" 
342+ 
343+             if  (prefix ==  null  ||  className ==  null  ||  fileName ==  null  || 
344+                 lineNumber ==  null  ||  stackElementText ==  null 
345+             ) {
346+                 sb.append(line).append(' \n ' 
347+                 continue 
348+             }
349+ 
350+             val  path =  className.substringAfter(" com.demonwav.mcdev." 
351+                 .substringBeforeLast(' .' 
352+                 .replace(' .' ' /' 
353+             sb.apply  {
354+                 append(prefix)
355+                 append(" <a href=\" " 
356+                 append(baseTagUrl)
357+                 append(path)
358+                 append(' /' 
359+                 append(fileName)
360+                 append(" #L" 
361+                 append(lineNumber)
362+                 append(" \" >" 
363+                 append(stackElementText)
364+                 append(" </a>\n " 
365+             }
366+         }
367+ 
368+         return  sb.toString()
369+     }
370+ 
284371    private  val  userAgent by lazy {
285372        var  agent =  " Minecraft Development IntelliJ IDEA plugin" 
286373
0 commit comments